Skip to content

feat(ci): add automated release workflow with patch version bumping (… #1

feat(ci): add automated release workflow with patch version bumping (…

feat(ci): add automated release workflow with patch version bumping (… #1

Workflow file for this run

name: Release Tag
on:
push:
tags:
- "v*.*.*"
permissions:
contents: write
packages: write
defaults:
run:
shell: bash
jobs:
build-gateway:
uses: ./.github/workflows/docker-build.yml
with:
component: gateway
build-cluster:
uses: ./.github/workflows/docker-build.yml
with:
component: cluster
tag-ghcr-release:
name: Tag GHCR Images for Release
needs: [build-gateway, build-cluster]
runs-on: build-amd64
timeout-minutes: 10
steps:
- name: Log in to GHCR
run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u "${{ github.actor }}" --password-stdin
- name: Tag images with version and latest
run: |
set -euo pipefail
REGISTRY="ghcr.io/nvidia/openshell"
VERSION="${GITHUB_REF_NAME#v}"
for component in gateway cluster; do
echo "Tagging ${REGISTRY}/${component}:${{ github.sha }} as ${VERSION} and latest..."
docker buildx imagetools create \
--prefer-index=false \
-t "${REGISTRY}/${component}:${VERSION}" \
"${REGISTRY}/${component}:${{ github.sha }}"
docker buildx imagetools create \
--prefer-index=false \
-t "${REGISTRY}/${component}:latest" \
"${REGISTRY}/${component}:${{ github.sha }}"
done
build-python-wheels:
name: Stage Python Wheels
needs: [build-gateway, build-cluster]
runs-on: build-amd64
timeout-minutes: 120
outputs:
wheel_version: ${{ steps.version.outputs.wheel_version }}
s3_prefix: ${{ steps.upload.outputs.s3_prefix }}
container:
image: ghcr.io/nvidia/openshell/ci:latest
credentials:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
options: --privileged
volumes:
- /var/run/docker.sock:/var/run/docker.sock
env:
MISE_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SCCACHE_MEMCACHED_ENDPOINT: ${{ vars.SCCACHE_MEMCACHED_ENDPOINT }}
NAV_PYPI_S3_BUCKET: navigator-pypi-artifacts # TODO: rename bucket to openshell-pypi-artifacts
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: us-west-2
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Log in to GHCR
run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u "${{ github.actor }}" --password-stdin
- name: Set up Docker Buildx
uses: ./.github/actions/setup-buildx
- name: Mark workspace safe for git
run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
- name: Fetch tags
run: git fetch --tags --force
- name: Compute Python version
id: version
run: |
set -euo pipefail
WHEEL_VERSION=$(uv run python tasks/scripts/release.py get-version --python)
echo "wheel_version=${WHEEL_VERSION}" >> "$GITHUB_OUTPUT"
- name: Build Python wheels
run: |
set -euo pipefail
WHEEL_VERSION="${{ steps.version.outputs.wheel_version }}"
CARGO_VERSION=$(uv run python tasks/scripts/release.py get-version --cargo)
OPENSHELL_CARGO_VERSION="$CARGO_VERSION" mise run python:build:multiarch
OPENSHELL_CARGO_VERSION="$CARGO_VERSION" mise run python:build:macos
ls -la target/wheels/*.whl
- name: Upload wheels to S3
id: upload
run: |
set -euo pipefail
WHEEL_VERSION="${{ steps.version.outputs.wheel_version }}"
S3_PREFIX="openshell/${WHEEL_VERSION}"
aws s3 cp target/wheels/ "s3://${NAV_PYPI_S3_BUCKET}/${S3_PREFIX}/" --recursive --exclude "*" --include "*.whl"
aws s3 ls "s3://${NAV_PYPI_S3_BUCKET}/${S3_PREFIX}/"
echo "s3_prefix=${S3_PREFIX}" >> "$GITHUB_OUTPUT"
publish-python:
name: Publish Python
needs: [build-python-wheels]
runs-on: [self-hosted, nv]
timeout-minutes: 10
env:
MISE_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NAV_PYPI_S3_BUCKET: navigator-pypi-artifacts # TODO: rename bucket to openshell-pypi-artifacts
NAV_PYPI_REPOSITORY_URL: https://urm.nvidia.com/artifactory/api/pypi/nv-shared-pypi-local
NAV_PYPI_USERNAME: ${{ secrets.NAV_PYPI_USERNAME }}
NAV_PYPI_PASSWORD: ${{ secrets.NAV_PYPI_PASSWORD }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: us-west-2
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-python@v5
with:
python-version: "3.13"
- name: Install publish dependencies
run: |
set -euo pipefail
python -m pip install --upgrade pip uv
if ! command -v aws >/dev/null 2>&1; then
ARCH="$(uname -m)"
case "$ARCH" in
x86_64|amd64) AWSCLI_ARCH="x86_64" ;;
aarch64|arm64) AWSCLI_ARCH="aarch64" ;;
*)
echo "Unsupported architecture for AWS CLI installer: $ARCH" >&2
exit 1
;;
esac
rm -rf aws awscliv2.zip
curl --fail --silent --show-error --location \
"https://awscli.amazonaws.com/awscli-exe-linux-${AWSCLI_ARCH}.zip" \
--output awscliv2.zip
unzip -q awscliv2.zip
./aws/install --install-dir "$HOME/.local/aws-cli" --bin-dir "$HOME/.local/bin" --update
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
export PATH="$HOME/.local/bin:$PATH"
fi
aws --version
uv --version
- name: List and download versioned wheels from S3
run: |
set -euo pipefail
WHEEL_VERSION="${{ needs.build-python-wheels.outputs.wheel_version }}"
S3_PREFIX="${{ needs.build-python-wheels.outputs.s3_prefix }}"
OBJECT_COUNT=$(aws s3api list-objects-v2 --bucket "$NAV_PYPI_S3_BUCKET" --prefix "${S3_PREFIX}/" --query "length(Contents)" --output text)
if [ "$OBJECT_COUNT" = "None" ] || [ "$OBJECT_COUNT" = "0" ]; then
echo "No wheel artifacts found for ${WHEEL_VERSION} at s3://${NAV_PYPI_S3_BUCKET}/${S3_PREFIX}/" >&2
exit 1
fi
aws s3api list-objects-v2 --bucket "$NAV_PYPI_S3_BUCKET" --prefix "${S3_PREFIX}/" --query "Contents[].Key" --output text
mkdir -p target/wheels
aws s3 cp "s3://${NAV_PYPI_S3_BUCKET}/${S3_PREFIX}/" target/wheels/ --recursive --exclude "*" --include "*.whl"
ls -la target/wheels/*.whl
- name: Publish wheels to Artifactory
run: |
set -euo pipefail
WHEEL_VERSION="${{ needs.build-python-wheels.outputs.wheel_version }}"
uv run python tasks/scripts/release.py python-publish --version "$WHEEL_VERSION"
# ---------------------------------------------------------------------------
# Build CLI binaries (Linux musl — static, native on each arch)
# ---------------------------------------------------------------------------
build-cli-linux:
name: Build CLI (Linux ${{ matrix.arch }})
strategy:
matrix:
include:
- arch: amd64
runner: build-amd64
target: x86_64-unknown-linux-musl
- arch: arm64
runner: build-arm64
target: aarch64-unknown-linux-musl
runs-on: ${{ matrix.runner }}
timeout-minutes: 60
container:
image: ghcr.io/nvidia/openshell/ci:latest
credentials:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
options: --privileged
env:
MISE_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SCCACHE_MEMCACHED_ENDPOINT: ${{ vars.SCCACHE_MEMCACHED_ENDPOINT }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Mark workspace safe for git
run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
- name: Fetch tags
run: git fetch --tags --force
- name: Install tools
run: mise install
- name: Cache Rust target and registry
uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2
with:
shared-key: cli-musl-${{ matrix.arch }}
cache-directories: .cache/sccache
cache-targets: "true"
- name: Compute version
id: version
run: |
set -euo pipefail
CARGO_VERSION=$(uv run python tasks/scripts/release.py get-version --cargo)
echo "cargo_version=${CARGO_VERSION}" >> "$GITHUB_OUTPUT"
- name: Install musl toolchain
run: |
set -euo pipefail
apt-get update
apt-get install -y --no-install-recommends musl-tools
rm -rf /var/lib/apt/lists/*
- name: Add Rust musl target
run: mise x -- rustup target add ${{ matrix.target }}
- name: Scope workspace to CLI crates
run: |
set -euo pipefail
# Remove workspace members that are not needed for openshell-cli.
# This avoids Cargo feature-unification pulling in aws-lc-sys (via
# russh in openshell-sandbox / openshell-server).
sed -i 's|members = \["crates/\*"\]|members = ["crates/openshell-cli", "crates/openshell-core", "crates/openshell-bootstrap", "crates/openshell-policy", "crates/openshell-providers", "crates/openshell-tui"]|' Cargo.toml
- name: Patch workspace version
if: steps.version.outputs.cargo_version != ''
run: |
set -euo pipefail
sed -i -E '/^\[workspace\.package\]/,/^\[/{s/^version[[:space:]]*=[[:space:]]*".*"/version = "'"${{ steps.version.outputs.cargo_version }}"'"/}' Cargo.toml
- name: Build ${{ matrix.target }}
run: mise x -- cargo build --release --target ${{ matrix.target }} -p openshell-cli
- name: sccache stats
if: always()
run: mise x -- sccache --show-stats
- name: Package binary
run: |
set -euo pipefail
mkdir -p artifacts
tar -czf artifacts/openshell-${{ matrix.target }}.tar.gz \
-C target/${{ matrix.target }}/release openshell
ls -lh artifacts/
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: cli-linux-${{ matrix.arch }}
path: artifacts/*.tar.gz
retention-days: 5
# ---------------------------------------------------------------------------
# Build CLI binary (macOS aarch64 via osxcross)
# ---------------------------------------------------------------------------
build-cli-macos:
name: Build CLI (macOS)
runs-on: build-amd64
timeout-minutes: 60
container:
image: ghcr.io/nvidia/openshell/ci:latest
credentials:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
options: --privileged
volumes:
- /var/run/docker.sock:/var/run/docker.sock
env:
MISE_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SCCACHE_MEMCACHED_ENDPOINT: ${{ vars.SCCACHE_MEMCACHED_ENDPOINT }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Mark workspace safe for git
run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
- name: Fetch tags
run: git fetch --tags --force
- name: Log in to GHCR
run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u "${{ github.actor }}" --password-stdin
- name: Set up Docker Buildx
uses: ./.github/actions/setup-buildx
- name: Compute version
id: version
run: |
set -euo pipefail
CARGO_VERSION=$(uv run python tasks/scripts/release.py get-version --cargo)
echo "cargo_version=${CARGO_VERSION}" >> "$GITHUB_OUTPUT"
- name: Build macOS binary via Docker
run: |
set -euo pipefail
docker buildx build \
--file deploy/docker/Dockerfile.cli-macos \
--build-arg OPENSHELL_CARGO_VERSION="${{ steps.version.outputs.cargo_version }}" \
--build-arg CARGO_TARGET_CACHE_SCOPE="${{ github.sha }}" \
--target binary \
--output type=local,dest=out/ \
.
- name: Package binary
run: |
set -euo pipefail
mkdir -p artifacts
tar -czf artifacts/openshell-aarch64-apple-darwin.tar.gz \
-C out openshell
ls -lh artifacts/
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: cli-macos
path: artifacts/*.tar.gz
retention-days: 5
# ---------------------------------------------------------------------------
# Create a tagged GitHub Release with CLI binaries
# ---------------------------------------------------------------------------
release:
name: Release
needs: [build-cli-linux, build-cli-macos, publish-python, tag-ghcr-release]
runs-on: build-amd64
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- name: Compute version
id: version
run: echo "semver=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"
- name: Download all CLI artifacts
uses: actions/download-artifact@v4
with:
pattern: cli-*
path: release/
merge-multiple: true
- name: Generate checksums
run: |
set -euo pipefail
cd release
sha256sum *.tar.gz > openshell-checksums-sha256.txt
cat openshell-checksums-sha256.txt
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
name: OpenShell ${{ github.ref_name }}
prerelease: false
tag_name: ${{ github.ref_name }}
generate_release_notes: false
body: |
## OpenShell ${{ github.ref_name }}
### Quick install
Requires the [GitHub CLI (`gh`)](https://cli.github.com) to be installed and authenticated.
```bash
sh -c 'ARCH=$(uname -m); OS=$(uname -s); \
case "${OS}-${ARCH}" in \
Linux-x86_64) ASSET="openshell-x86_64-unknown-linux-musl.tar.gz" ;; \
Linux-aarch64) ASSET="openshell-aarch64-unknown-linux-musl.tar.gz" ;; \
Darwin-arm64) ASSET="openshell-aarch64-apple-darwin.tar.gz" ;; \
*) echo "Unsupported platform: ${OS}-${ARCH}" >&2; exit 1 ;; \
esac; \
gh release download ${{ github.ref_name }} --repo NVIDIA/OpenShell --pattern "${ASSET}" -O - \
| tar xz \
&& sudo install -m 755 openshell /usr/local/bin/openshell'
```
### Docker images
```bash
docker pull ghcr.io/nvidia/openshell/gateway:${{ steps.version.outputs.semver }}
docker pull ghcr.io/nvidia/openshell/cluster:${{ steps.version.outputs.semver }}
```
### Assets
| File | Platform |
|------|----------|
| `openshell-x86_64-unknown-linux-musl.tar.gz` | Linux x86_64 |
| `openshell-aarch64-unknown-linux-musl.tar.gz` | Linux aarch64 / ARM64 |
| `openshell-aarch64-apple-darwin.tar.gz` | macOS Apple Silicon |
| `openshell-checksums-sha256.txt` | SHA256 checksums for all archives |
files: |
release/openshell-x86_64-unknown-linux-musl.tar.gz
release/openshell-aarch64-unknown-linux-musl.tar.gz
release/openshell-aarch64-apple-darwin.tar.gz
release/openshell-checksums-sha256.txt