Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
141 changes: 141 additions & 0 deletions .github/scripts/update_version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
#!/usr/bin/env python3
"""
Script to automatically increment Test PyPI package versions.

This script fetches the latest version from Test PyPI, increments the alpha version,
and updates the pyproject.toml file accordingly.
"""

import requests
import re
import sys
from pathlib import Path


def get_latest_version(package_name="nilai-py"):
"""
Fetch the latest version from Test PyPI.

Args:
package_name: Name of the package to check

Returns:
str: Latest version string, or "0.0.0a0" if package doesn't exist
"""
try:
response = requests.get(
f"https://test.pypi.org/pypi/{package_name}/json", timeout=10
)
if response.status_code == 404:
# Package doesn't exist yet, start with 0.0.0a1
print(f"Package {package_name} not found on Test PyPI, starting fresh")
return "0.0.0a0"

response.raise_for_status()
data = response.json()
versions = list(data["releases"].keys())

if not versions:
print("No versions found, starting fresh")
return "0.0.0a0"

# Filter for alpha versions and find the latest
alpha_versions = [v for v in versions if "a" in v]
if not alpha_versions:
print("No alpha versions found, starting fresh")
return "0.0.0a0"

# Sort versions and get the latest
alpha_versions.sort(key=lambda x: [int(i) for i in re.findall(r"\d+", x)])
latest = alpha_versions[-1]
print(f"Found latest alpha version: {latest}")
return latest

except Exception as e:
print(f"Error fetching version: {e}")
return "0.0.0a0"


def increment_version(version):
"""
Increment the alpha version number.

Args:
version: Version string like "0.0.0a1"

Returns:
str: Incremented version string like "0.0.0a2"
"""
# Parse version like "0.0.0a1" or "0.1.0a5"
match = re.match(r"(\d+)\.(\d+)\.(\d+)a(\d+)", version)
if match:
major, minor, patch, alpha = match.groups()
new_alpha = int(alpha) + 1
new_version = f"{major}.{minor}.{patch}a{new_alpha}"
print(f"Incrementing {version} -> {new_version}")
return new_version
else:
# If no match, start with a1
print(f"Could not parse version {version}, defaulting to 0.0.0a1")
return "0.0.0a1"


def update_pyproject_version(new_version, pyproject_path="pyproject.toml"):
"""
Update the version in pyproject.toml file.

Args:
new_version: New version string to set
pyproject_path: Path to pyproject.toml file

Returns:
str: The new version that was set
"""
pyproject_file = Path(pyproject_path)

if not pyproject_file.exists():
raise FileNotFoundError(f"Could not find {pyproject_path}")

content = pyproject_file.read_text()

# Update version line
updated_content = re.sub(
r'^version = ".*"', f'version = "{new_version}"', content, flags=re.MULTILINE
)

if content == updated_content:
print("Warning: No version line found to update in pyproject.toml")

pyproject_file.write_text(updated_content)
print(f"Updated {pyproject_path} with version {new_version}")
return new_version


def main():
"""Main function to orchestrate version update."""
print("=== Updating package version ===")

# Get latest version from Test PyPI
latest_version = get_latest_version()
print(f"Latest version from Test PyPI: {latest_version}")

# Increment version
new_version = increment_version(latest_version)
print(f"New version: {new_version}")

# Update pyproject.toml
update_pyproject_version(new_version)

# Output for GitHub Actions (using newer syntax)
print(f"NEW_VERSION={new_version}")

return new_version


if __name__ == "__main__":
try:
version = main()
Copy link

Copilot AI Oct 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The global variable 'version' is not used.

Suggested change
version = main()
main()

Copilot uses AI. Check for mistakes.
sys.exit(0)
except Exception as e:
print(f"Error: {e}")
sys.exit(1)
142 changes: 142 additions & 0 deletions .github/scripts/update_version_from_release.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
#!/usr/bin/env python3
"""
Script to update pyproject.toml version based on GitHub release tag.

This script takes a release tag (like 'v1.0.0' or '1.0.0') and updates
the version field in pyproject.toml accordingly.
"""

import re
import sys
import argparse
from pathlib import Path


def normalize_version(tag_version):
"""
Normalize a version tag to a clean version string.

Args:
tag_version: Version from GitHub release tag (e.g., 'v1.0.0', '1.0.0', 'v1.0.0-beta.1')

Returns:
str: Clean version string (e.g., '1.0.0', '1.0.0b1')
"""
# Remove 'v' prefix if present
version = tag_version.lstrip("v")

# Convert beta/alpha/rc notation to PEP 440 format
# v1.0.0-beta.1 -> 1.0.0b1
# v1.0.0-alpha.2 -> 1.0.0a2
# v1.0.0-rc.1 -> 1.0.0rc1
version = re.sub(r"-beta\.?(\d+)", r"b\1", version)
version = re.sub(r"-alpha\.?(\d+)", r"a\1", version)
version = re.sub(r"-rc\.?(\d+)", r"rc\1", version)

print(f"Normalized version: {tag_version} -> {version}")
return version


def validate_version(version):
"""
Validate that the version follows PEP 440 format.

Args:
version: Version string to validate

Returns:
bool: True if valid, False otherwise
"""
# Basic PEP 440 version pattern
pattern = r"^([1-9][0-9]*!)?(0|[1-9][0-9]*)(\.(0|[1-9][0-9]*))*((a|b|rc)(0|[1-9][0-9]*))?(\.post(0|[1-9][0-9]*))?(\.dev(0|[1-9][0-9]*))?$"

if re.match(pattern, version):
print(f"Version {version} is valid")
return True
else:
print(f"Warning: Version {version} may not be PEP 440 compliant")
return False


def update_pyproject_version(new_version, pyproject_path="pyproject.toml"):
"""
Update the version in pyproject.toml file.

Args:
new_version: New version string to set
pyproject_path: Path to pyproject.toml file

Returns:
str: The new version that was set
"""
pyproject_file = Path(pyproject_path)

if not pyproject_file.exists():
raise FileNotFoundError(f"Could not find {pyproject_path}")

content = pyproject_file.read_text()
original_content = content

# Update version line
updated_content = re.sub(
r'^version = ".*"', f'version = "{new_version}"', content, flags=re.MULTILINE
)

if content == updated_content:
raise ValueError("No version line found to update in pyproject.toml")

pyproject_file.write_text(updated_content)
print(f"Updated {pyproject_path} with version {new_version}")

# Show the change
old_version_match = re.search(r'^version = "(.*)"', original_content, re.MULTILINE)
if old_version_match:
old_version = old_version_match.group(1)
print(f"Version changed: {old_version} -> {new_version}")

return new_version


def main():
"""Main function to orchestrate version update from release tag."""
parser = argparse.ArgumentParser(
description="Update pyproject.toml version from GitHub release tag"
)
parser.add_argument(
"tag_version", help="The release tag version (e.g., 'v1.0.0' or '1.0.0')"
)
parser.add_argument(
"--pyproject", default="pyproject.toml", help="Path to pyproject.toml file"
)
parser.add_argument(
"--validate", action="store_true", help="Validate version format"
)

args = parser.parse_args()

print("=== Updating version from release tag ===")
print(f"Release tag: {args.tag_version}")

# Normalize the version
normalized_version = normalize_version(args.tag_version)

# Validate if requested
if args.validate:
validate_version(normalized_version)

# Update pyproject.toml
try:
update_pyproject_version(normalized_version, args.pyproject)
print(f"SUCCESS: Updated version to {normalized_version}")

# Output for GitHub Actions
print(f"RELEASE_VERSION={normalized_version}")

return 0
except Exception as e:
print(f"ERROR: {e}")
return 1


if __name__ == "__main__":
sys.exit(main())
19 changes: 17 additions & 2 deletions .github/workflows/cicd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,21 @@ jobs:
- component: api
build_args: "--target nilai --platform linux/amd64"
steps:
- name: Disable unattended-upgrades
run: |
echo "Disabling unattended-upgrades to prevent dpkg lock issues..."
# Stop and disable the unattended-upgrades service
sudo systemctl stop unattended-upgrades || true
sudo systemctl disable unattended-upgrades || true
sudo systemctl mask unattended-upgrades || true
# Kill any running unattended-upgrades processes
sudo killall -9 unattended-upgrade apt apt-get dpkg || true
# Remove any stale locks
sudo rm -f /var/lib/dpkg/lock-frontend /var/lib/dpkg/lock /var/cache/apt/archives/lock /var/lib/apt/lists/lock || true
# Reconfigure dpkg in case it was interrupted
sudo dpkg --configure -a || true
echo "unattended-upgrades disabled successfully"

- name: Checkout
uses: actions/checkout@v2

Expand Down Expand Up @@ -266,7 +281,7 @@ jobs:
docker ps -a

- name: Wait for services to be healthy
run: bash scripts/wait_for_ci_services.sh
run: bash .github/scripts/wait_for_ci_services.sh

- name: Run E2E tests for NUC
run: |
Expand All @@ -279,7 +294,7 @@ jobs:
run: |
set -e
# Create a user with a rate limit of 1000 requests per minute, hour, and day
export AUTH_TOKEN=$(docker exec nilai-api uv run src/nilai_api/commands/add_user.py --name test1 --ratelimit-minute 1000 --ratelimit-hour 1000 --ratelimit-day 1000 | jq ".apikey" -r)
export AUTH_TOKEN=$(docker exec nilai-api uv run src/nilai_api/commands/add_user.py --name test1 --ratelimit-minute 1000 --ratelimit-hour 1000 --ratelimit-day 1000 --apikey SecretTestApiKey | jq ".apikey" -r)
export ENVIRONMENT=ci
# Set the environment variable for the API key
export AUTH_STRATEGY=api_key
Expand Down
69 changes: 69 additions & 0 deletions .github/workflows/pypi-publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
name: Publish nilai-py to PyPI

on:
release:
types: [published]
workflow_dispatch: # Allow manual trigger for testing

jobs:
pypi-publish:
name: Publish nilai-py to PyPI
runs-on: ubuntu-latest

# Only run on published releases (not drafts) and only for nilai-py releases
if: github.event.release.draft == false && startsWith(github.event.release.tag_name, 'nilai-py-v')

steps:
- uses: actions/checkout@v4

- name: Install uv
uses: astral-sh/setup-uv@v5

- name: Set up Python
run: uv python install

- name: Install dependencies
working-directory: clients/nilai-py
run: uv sync --all-extras --dev

- name: Run tests
working-directory: clients/nilai-py
run: uv run pytest tests/

- name: Update version from release tag
id: version
working-directory: clients/nilai-py
run: |
# Get the release tag (remove refs/tags/ prefix if present)
RELEASE_TAG="${{ github.event.release.tag_name }}"
echo "Release tag: $RELEASE_TAG"

# Update pyproject.toml with the release version
RELEASE_VERSION=$(uv run python ../../.github/scripts/update_version_from_release.py "$RELEASE_TAG" --validate | grep "RELEASE_VERSION=" | cut -d'=' -f2)
echo "release_version=$RELEASE_VERSION" >> $GITHUB_OUTPUT
echo "Updated version to: $RELEASE_VERSION"

- name: Verify version update
working-directory: clients/nilai-py
run: |
# Show the updated version in pyproject.toml
grep "^version = " pyproject.toml
echo "Building package with version: ${{ steps.version.outputs.release_version }}"

- name: Build package
working-directory: clients/nilai-py
run: uv build

- name: Publish to PyPI
working-directory: clients/nilai-py
env:
UV_PUBLISH_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
run: |
echo "Publishing to PyPI..."
uv publish

- name: Create GitHub release comment
if: success()
run: |
echo "✅ Successfully published nilai-py v${{ steps.version.outputs.release_version }} to PyPI!" >> $GITHUB_STEP_SUMMARY
echo "📦 Package: https://pypi.org/project/nilai-py/${{ steps.version.outputs.release_version }}/" >> $GITHUB_STEP_SUMMARY
Loading
Loading