Skip to content

Commit 0535fe8

Browse files
committed
fix(release): stabilize dev build assets
1 parent d8b8477 commit 0535fe8

9 files changed

Lines changed: 282 additions & 60 deletions

File tree

.github/workflows/release-dev.yml

Lines changed: 55 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ jobs:
3030
python_version: ${{ steps.v.outputs.python }}
3131
cargo_version: ${{ steps.v.outputs.cargo }}
3232
deb_version: ${{ steps.v.outputs.deb }}
33+
rpm_version: ${{ steps.v.outputs.rpm_version }}
34+
rpm_release: ${{ steps.v.outputs.rpm_release }}
3335
steps:
3436
- uses: actions/checkout@v6
3537
with:
@@ -48,6 +50,8 @@ jobs:
4850
echo "python=$(uv run python tasks/scripts/release.py get-version --python)" >> "$GITHUB_OUTPUT"
4951
echo "cargo=$(uv run python tasks/scripts/release.py get-version --cargo)" >> "$GITHUB_OUTPUT"
5052
echo "deb=$(uv run python tasks/scripts/release.py get-version --deb)" >> "$GITHUB_OUTPUT"
53+
echo "rpm_version=$(uv run python tasks/scripts/release.py get-version --rpm-version)" >> "$GITHUB_OUTPUT"
54+
echo "rpm_release=$(uv run python tasks/scripts/release.py get-version --rpm-release)" >> "$GITHUB_OUTPUT"
5155
5256
build-gateway:
5357
needs: [compute-versions]
@@ -641,6 +645,10 @@ jobs:
641645
uses: ./.github/workflows/rpm-package.yml
642646
with:
643647
checkout-ref: ${{ github.sha }}
648+
rpm-version: ${{ needs.compute-versions.outputs.rpm_version }}
649+
rpm-release: ${{ needs.compute-versions.outputs.rpm_release }}
650+
cargo-version: ${{ needs.compute-versions.outputs.cargo_version }}
651+
python-version: ${{ needs.compute-versions.outputs.python_version }}
644652
secrets: inherit
645653

646654
# ---------------------------------------------------------------------------
@@ -698,6 +706,31 @@ jobs:
698706
path: release/
699707
merge-multiple: true
700708

709+
- name: Normalize dev package filenames
710+
run: |
711+
set -euo pipefail
712+
shopt -s nullglob
713+
714+
move_one() {
715+
local dest="$1"
716+
shift
717+
local matches=("$@")
718+
if [ "${#matches[@]}" -ne 1 ]; then
719+
echo "expected exactly one source for ${dest}, found ${#matches[@]}: ${matches[*]-}" >&2
720+
exit 1
721+
fi
722+
mv "${matches[0]}" "release/${dest}"
723+
}
724+
725+
move_one openshell-dev-amd64.deb release/openshell_*_amd64.deb
726+
move_one openshell-dev-arm64.deb release/openshell_*_arm64.deb
727+
move_one openshell-dev-x86_64.rpm release/openshell-[0-9]*.x86_64.rpm
728+
move_one openshell-dev-aarch64.rpm release/openshell-[0-9]*.aarch64.rpm
729+
move_one openshell-gateway-dev-x86_64.rpm release/openshell-gateway-[0-9]*.x86_64.rpm
730+
move_one openshell-gateway-dev-aarch64.rpm release/openshell-gateway-[0-9]*.aarch64.rpm
731+
732+
ls -la release/
733+
701734
- name: Capture wheel filenames
702735
id: wheel_filenames
703736
run: |
@@ -714,7 +747,7 @@ jobs:
714747
openshell-x86_64-unknown-linux-musl.tar.gz \
715748
openshell-aarch64-unknown-linux-musl.tar.gz \
716749
openshell-aarch64-apple-darwin.tar.gz \
717-
openshell_*.deb \
750+
*.deb \
718751
openshell-*.rpm \
719752
*.whl > openshell-checksums-sha256.txt
720753
cat openshell-checksums-sha256.txt
@@ -728,20 +761,12 @@ jobs:
728761
openshell-sandbox-aarch64-unknown-linux-gnu.tar.gz > openshell-sandbox-checksums-sha256.txt
729762
cat openshell-sandbox-checksums-sha256.txt
730763
731-
- name: Prune stale wheel, deb, and rpm assets from dev release
764+
- name: Prune managed assets from dev release
732765
uses: actions/github-script@v7
733-
env:
734-
WHEEL_VERSION: ${{ needs.compute-versions.outputs.python_version }}
735766
with:
736767
script: |
737-
const wheelVersion = process.env.WHEEL_VERSION;
738-
const currentPrefix = `openshell-${wheelVersion}-`;
739768
const [owner, repo] = process.env.GITHUB_REPOSITORY.split('/');
740769
741-
core.info(`=== Wheel pruning diagnostics ===`);
742-
core.info(`WHEEL_VERSION: ${wheelVersion}`);
743-
core.info(`CURRENT_PREFIX: ${currentPrefix}`);
744-
745770
// Fetch the dev release
746771
let release;
747772
try {
@@ -760,27 +785,29 @@ jobs:
760785
core.info(` ${String(a.id).padStart(12)} ${a.name}`);
761786
}
762787
763-
// Delete stale wheels, debs, and rpms
764-
let kept = 0, deleted = 0, debDeleted = 0, rpmDeleted = 0;
788+
const managed = (name) => (
789+
name.startsWith('openshell') &&
790+
(
791+
name.endsWith('.tar.gz') ||
792+
name.endsWith('.txt') ||
793+
name.endsWith('.whl') ||
794+
name.endsWith('.deb') ||
795+
name.endsWith('.rpm')
796+
)
797+
);
798+
799+
let deleted = 0, skipped = 0;
765800
for (const asset of assets) {
766-
if (asset.name.endsWith('.deb')) {
767-
core.info(`Deleting stale deb package: ${asset.name} (id=${asset.id})`);
768-
await github.rest.repos.deleteReleaseAsset({ owner, repo, asset_id: asset.id });
769-
debDeleted++;
770-
} else if (asset.name.endsWith('.rpm')) {
771-
core.info(`Deleting stale rpm package: ${asset.name} (id=${asset.id})`);
772-
await github.rest.repos.deleteReleaseAsset({ owner, repo, asset_id: asset.id });
773-
rpmDeleted++;
774-
} else if (asset.name.endsWith('.whl') && asset.name.startsWith(currentPrefix)) {
775-
core.info(`Keeping current wheel: ${asset.name}`);
776-
kept++;
777-
} else if (asset.name.endsWith('.whl')) {
778-
core.info(`Deleting stale wheel: ${asset.name} (id=${asset.id})`);
801+
if (managed(asset.name)) {
802+
core.info(`Deleting managed dev asset: ${asset.name} (id=${asset.id})`);
779803
await github.rest.repos.deleteReleaseAsset({ owner, repo, asset_id: asset.id });
780804
deleted++;
805+
} else {
806+
core.info(`Skipping unmanaged asset: ${asset.name}`);
807+
skipped++;
781808
}
782809
}
783-
core.info(`Summary: kept_wheels=${kept}, deleted_wheels=${deleted}, deleted_debs=${debDeleted}, deleted_rpms=${rpmDeleted}`);
810+
core.info(`Summary: deleted=${deleted}, skipped=${skipped}`);
784811
785812
- name: Move dev tag
786813
run: |
@@ -811,7 +838,8 @@ jobs:
811838
release/openshell-x86_64-unknown-linux-musl.tar.gz
812839
release/openshell-aarch64-unknown-linux-musl.tar.gz
813840
release/openshell-aarch64-apple-darwin.tar.gz
814-
release/openshell_*.deb
841+
release/openshell-dev-amd64.deb
842+
release/openshell-dev-arm64.deb
815843
release/openshell-*.rpm
816844
release/openshell-gateway-x86_64-unknown-linux-gnu.tar.gz
817845
release/openshell-gateway-aarch64-unknown-linux-gnu.tar.gz

.github/workflows/release-tag.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ jobs:
4141
python_version: ${{ steps.v.outputs.python }}
4242
cargo_version: ${{ steps.v.outputs.cargo }}
4343
deb_version: ${{ steps.v.outputs.deb }}
44+
rpm_version: ${{ steps.v.outputs.rpm_version }}
45+
rpm_release: ${{ steps.v.outputs.rpm_release }}
4446
# Semver without 'v' prefix (e.g. 0.6.0), used for image tags and release body
4547
semver: ${{ steps.v.outputs.semver }}
4648
steps:
@@ -62,6 +64,8 @@ jobs:
6264
echo "python=$(uv run python tasks/scripts/release.py get-version --python)" >> "$GITHUB_OUTPUT"
6365
echo "cargo=$(uv run python tasks/scripts/release.py get-version --cargo)" >> "$GITHUB_OUTPUT"
6466
echo "deb=$(uv run python tasks/scripts/release.py get-version --deb)" >> "$GITHUB_OUTPUT"
67+
echo "rpm_version=$(uv run python tasks/scripts/release.py get-version --rpm-version)" >> "$GITHUB_OUTPUT"
68+
echo "rpm_release=$(uv run python tasks/scripts/release.py get-version --rpm-release)" >> "$GITHUB_OUTPUT"
6569
echo "semver=${RELEASE_TAG#v}" >> "$GITHUB_OUTPUT"
6670
6771
build-gateway:
@@ -668,6 +672,10 @@ jobs:
668672
uses: ./.github/workflows/rpm-package.yml
669673
with:
670674
checkout-ref: ${{ inputs.tag || github.ref }}
675+
rpm-version: ${{ needs.compute-versions.outputs.rpm_version }}
676+
rpm-release: ${{ needs.compute-versions.outputs.rpm_release }}
677+
cargo-version: ${{ needs.compute-versions.outputs.cargo_version }}
678+
python-version: ${{ needs.compute-versions.outputs.python_version }}
671679
secrets: inherit
672680

673681
# ---------------------------------------------------------------------------

.github/workflows/rpm-package.yml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,22 @@ on:
99
checkout-ref:
1010
required: true
1111
type: string
12+
rpm-version:
13+
required: false
14+
type: string
15+
default: ""
16+
rpm-release:
17+
required: false
18+
type: string
19+
default: ""
20+
cargo-version:
21+
required: false
22+
type: string
23+
default: ""
24+
python-version:
25+
required: false
26+
type: string
27+
default: ""
1228

1329
permissions:
1430
contents: read
@@ -53,6 +69,11 @@ jobs:
5369
run: git fetch --tags --force
5470

5571
- name: Build RPMs via Packit
72+
env:
73+
OPENSHELL_RPM_VERSION: ${{ inputs['rpm-version'] }}
74+
OPENSHELL_RPM_RELEASE: ${{ inputs['rpm-release'] }}
75+
OPENSHELL_CARGO_VERSION: ${{ inputs['cargo-version'] }}
76+
OPENSHELL_PYTHON_VERSION: ${{ inputs['python-version'] }}
5677
run: packit build locally
5778

5879
- name: Collect RPM artifacts

.packit.yaml

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,10 @@ srpm_build_deps:
1717

1818
actions:
1919
get-current-version:
20-
# Derive version from the latest upstream tag on the current branch.
21-
- 'bash -c "git describe --tags --match ''v*'' --abbrev=0 HEAD | sed ''s/^v//''"'
20+
# Release workflows pass OPENSHELL_RPM_VERSION so every artifact shares one
21+
# precomputed identity. Local Packit runs fall back to the latest semantic
22+
# upstream tag on the current branch.
23+
- 'bash -c "if [ -n \"${OPENSHELL_RPM_VERSION:-}\" ]; then echo \"${OPENSHELL_RPM_VERSION}\"; else git describe --tags --match ''v[0-9]*.[0-9]*.[0-9]*'' --abbrev=0 HEAD | sed ''s/^v//''; fi"'
2224

2325
create-archive:
2426
# Step 1: Create source tarball from git working tree.
@@ -38,7 +40,10 @@ actions:
3840
# Update Version
3941
- 'bash -c "sed -i -r \"s/^Version:(\\s*)\\S+/Version:\\1${PACKIT_RPMSPEC_VERSION}/\" openshell.spec"'
4042
# Update Release
41-
- 'bash -c "sed -i -r \"s/^Release:(\\s*)\\S+/Release:\\1${PACKIT_RPMSPEC_RELEASE}%{?dist}/\" openshell.spec"'
43+
- 'bash -c "RELEASE=${OPENSHELL_RPM_RELEASE:-${PACKIT_RPMSPEC_RELEASE}} && sed -i -r \"s/^Release:(\\s*)\\S+/Release:\\1${RELEASE}%{?dist}/\" openshell.spec"'
44+
# Keep embedded binary / Python metadata aligned with the release workflow.
45+
- 'bash -c "if [ -n \"${OPENSHELL_CARGO_VERSION:-}\" ]; then sed -i -r \"s/^%global openshell_cargo_version .*/%global openshell_cargo_version ${OPENSHELL_CARGO_VERSION}/\" openshell.spec; fi"'
46+
- 'bash -c "if [ -n \"${OPENSHELL_PYTHON_VERSION:-}\" ]; then sed -i -r \"s/^%global openshell_python_version .*/%global openshell_python_version ${OPENSHELL_PYTHON_VERSION}/\" openshell.spec; fi"'
4247

4348
jobs:
4449
# Build on every pull request targeting main for CI validation

architecture/build-containers.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,22 @@ OpenShell also publishes Python wheels for `linux/amd64`, `linux/arm64`, and mac
6666
- Release workflows mirror the CLI layout: a Linux matrix job for amd64/arm64, a separate macOS job, and release jobs that download the per-platform wheel artifacts directly before publishing.
6767
- Release CPU jobs run on `linux-amd64-cpu8` and `linux-arm64-cpu8`; the macOS wheel is still cross-compiled in Docker from the amd64 Linux runner.
6868

69+
## Development Release Assets
70+
71+
The rolling `dev` release is installer-facing but still publishes the full
72+
artifact set: CLI tarballs, standalone gateway and sandbox tarballs, Python
73+
wheels, Debian packages, RPM packages, and checksums. Every artifact is built
74+
from the version computed once in `release-dev.yml`.
75+
76+
Package-manager artifacts use stable dev aliases on the GitHub release
77+
(`openshell-dev-*.deb`, `openshell-dev-*.rpm`, and
78+
`openshell-gateway-dev-*.rpm`) so the rolling release stays readable. Python
79+
wheels keep their versioned filenames because wheel metadata requires it.
80+
81+
The dev release workflow prunes workflow-owned `openshell*` assets before
82+
uploading the fresh set. `openshell-driver-vm` artifacts are intentionally not
83+
published on the main `dev` release; VM driver binaries live on `vm-dev`.
84+
6985
## Sandbox Images
7086

7187
Sandbox images are not built in this repository. They are maintained in the [openshell-community](https://github.com/nvidia/openshell-community) repository and pulled from `ghcr.io/nvidia/openshell-community/sandboxes/` at runtime.

architecture/custom-vm-runtime.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,10 @@ run `cargo build --release -p openshell-driver-vm`. The macOS driver is
321321
cross-compiled via osxcross (no macOS runner needed for the binary build —
322322
only for the kernel build).
323323

324+
`openshell-driver-vm` release assets stay on `vm-dev`. The main rolling `dev`
325+
release prunes stale VM driver tarballs if they appear there, but does not
326+
publish new ones.
327+
324328
macOS driver binaries produced via osxcross are not codesigned. Development
325329
builds are signed automatically by `tasks/scripts/gateway-vm.sh`
326330
(registered as `mise run gateway:vm`); a packaged release needs signing in

openshell.spec

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
# SPDX-License-Identifier: Apache-2.0
33

44
%global crate openshell
5+
%global openshell_cargo_version %{version}
6+
%global openshell_python_version %{version}
57

68
# Cargo/Rust builds with vendored deps do not produce debugsource listings
79
# in the format redhat-rpm-config expects (especially on EPEL).
@@ -87,9 +89,9 @@ management, agent execution, and inference routing via gRPC.
8789
tar xf %{SOURCE1}
8890
%cargo_prep -v vendor
8991

90-
# Patch workspace version from placeholder to actual version
91-
sed -i 's/^version = "0.0.0"/version = "%{version}"/' Cargo.toml
92-
grep -q 'version = "%{version}"' Cargo.toml || (echo "ERROR: Cargo.toml version patch failed" && exit 1)
92+
# Patch workspace version from placeholder to actual build identity.
93+
sed -i 's/^version = "0.0.0"/version = "%{openshell_cargo_version}"/' Cargo.toml
94+
grep -q 'version = "%{openshell_cargo_version}"' Cargo.toml || (echo "ERROR: Cargo.toml version patch failed" && exit 1)
9395

9496
%build
9597
# Build the CLI and gateway binaries
@@ -203,11 +205,11 @@ install -pm 0644 python/%{name}/_proto/__init__.py %{buildroot}%{python3_sitelib
203205
install -pm 0644 python/%{name}/_proto/*.py %{buildroot}%{python3_sitelib}/%{name}/_proto/
204206

205207
# Create dist-info so importlib.metadata can resolve the package version
206-
install -d %{buildroot}%{python3_sitelib}/%{name}-%{version}.dist-info
207-
cat > %{buildroot}%{python3_sitelib}/%{name}-%{version}.dist-info/METADATA << EOF
208+
install -d %{buildroot}%{python3_sitelib}/%{name}-%{openshell_python_version}.dist-info
209+
cat > %{buildroot}%{python3_sitelib}/%{name}-%{openshell_python_version}.dist-info/METADATA << EOF
208210
Metadata-Version: 2.1
209211
Name: %{name}
210-
Version: 0.0.37
212+
Version: %{openshell_python_version}
211213
Summary: OpenShell Python SDK for agent execution and management
212214
License: Apache-2.0
213215
Requires-Python: >=3.12
@@ -217,10 +219,10 @@ Requires-Dist: protobuf>=4.25
217219
EOF
218220

219221
# INSTALLER marker per PEP 376
220-
echo "rpm" > %{buildroot}%{python3_sitelib}/%{name}-%{version}.dist-info/INSTALLER
222+
echo "rpm" > %{buildroot}%{python3_sitelib}/%{name}-%{openshell_python_version}.dist-info/INSTALLER
221223

222224
# RECORD can be empty for RPM-managed installs
223-
touch %{buildroot}%{python3_sitelib}/%{name}-%{version}.dist-info/RECORD
225+
touch %{buildroot}%{python3_sitelib}/%{name}-%{openshell_python_version}.dist-info/RECORD
224226

225227
%check
226228
# Smoke-test the CLI binary
@@ -233,7 +235,7 @@ touch %{buildroot}%{python3_sitelib}/%{name}-%{version}.dist-info/RECORD
233235
# We query the dist-info directly rather than importing the package because
234236
# the full import pulls in grpcio and other runtime deps not present in the
235237
# build environment.
236-
PYTHONPATH=%{buildroot}%{python3_sitelib} %{python3} -c "from importlib.metadata import version; v = version('openshell'); print(v); assert v == '%{version}', f'expected %{version}, got {v}'"
238+
PYTHONPATH=%{buildroot}%{python3_sitelib} %{python3} -c "from importlib.metadata import version; v = version('openshell'); print(v); assert v == '%{openshell_python_version}', f'expected %{openshell_python_version}, got {v}'"
237239

238240
%post gateway
239241
%systemd_user_post %{name}-gateway.service
@@ -269,7 +271,7 @@ PYTHONPATH=%{buildroot}%{python3_sitelib} %{python3} -c "from importlib.metadata
269271
%files -n python3-%{name}
270272
%license LICENSE
271273
%{python3_sitelib}/%{name}/
272-
%{python3_sitelib}/%{name}-%{version}.dist-info/
274+
%{python3_sitelib}/%{name}-%{openshell_python_version}.dist-info/
273275

274276
%changelog
275277
%autochangelog

python/release_tooling_test.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
from __future__ import annotations
5+
6+
import importlib.util
7+
import sys
8+
from pathlib import Path
9+
10+
11+
def _load_release_module():
12+
path = Path(__file__).resolve().parents[1] / "tasks/scripts/release.py"
13+
spec = importlib.util.spec_from_file_location("openshell_release_tooling", path)
14+
assert spec is not None
15+
assert spec.loader is not None
16+
module = importlib.util.module_from_spec(spec)
17+
sys.modules[spec.name] = module
18+
spec.loader.exec_module(module)
19+
return module
20+
21+
22+
release = _load_release_module()
23+
24+
25+
def test_exact_tag_versions_are_stable_release_versions() -> None:
26+
versions = release._versions_from_parts((0, 0, 37), 0, "152d05940", "v0.0.37")
27+
28+
assert versions.python == "0.0.37"
29+
assert versions.cargo == "0.0.37"
30+
assert versions.docker == "0.0.37"
31+
assert versions.deb == "0.0.37-1"
32+
assert versions.rpm_version == "0.0.37"
33+
assert versions.rpm_release == "1"
34+
35+
36+
def test_dev_versions_share_one_build_identity() -> None:
37+
versions = release._versions_from_parts((0, 0, 37), 108, "152d05940", "v0.0.37")
38+
39+
assert versions.python == "0.0.38.dev108+g152d05940"
40+
assert versions.cargo == "0.0.38-dev.108+g152d05940"
41+
assert versions.docker == "0.0.38-dev.108-g152d05940"
42+
assert versions.deb == "0.0.38~dev.108+g152d05940-1"
43+
assert versions.rpm_version == "0.0.38"
44+
assert versions.rpm_release == "0.dev.108.g152d05940"
45+
46+
47+
def test_semver_tag_parser_excludes_vm_tags() -> None:
48+
assert release._parse_semver_tag("v0.0.37") == (0, 0, 37)
49+
assert release._parse_semver_tag("0.0.37") == (0, 0, 37)
50+
assert release._parse_semver_tag("vm-runtime") is None
51+
assert release._parse_semver_tag("vm-dev") is None

0 commit comments

Comments
 (0)