diff --git a/.github/workflows/release-tag.yml b/.github/workflows/release-tag.yml index 09010d9031..bc6291c094 100644 --- a/.github/workflows/release-tag.yml +++ b/.github/workflows/release-tag.yml @@ -407,7 +407,6 @@ jobs: cat openshell-supervisor-checksums-sha256.txt - name: Generate Homebrew formula - if: needs.compute-versions.outputs.is_prerelease != 'true' run: | set -euo pipefail python3 tasks/scripts/release.py generate-homebrew-formula \ @@ -493,12 +492,64 @@ jobs: release/openshell-supervisor-checksums-sha256.txt release/openshell-prover-checksums-sha256.txt - - name: Upload pre-release artifacts + - name: Upload amd64 Debian prerelease package if: needs.compute-versions.outputs.is_prerelease == 'true' uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: - name: openshell-${{ env.RELEASE_TAG }} - path: release/ + name: openshell-${{ env.RELEASE_TAG }}-linux-amd64-deb + path: | + release/openshell_*_amd64.deb + release/openshell-checksums-sha256.txt + retention-days: 90 + if-no-files-found: error + + - name: Upload arm64 Debian prerelease package + if: needs.compute-versions.outputs.is_prerelease == 'true' + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: openshell-${{ env.RELEASE_TAG }}-linux-arm64-deb + path: | + release/openshell_*_arm64.deb + release/openshell-checksums-sha256.txt + retention-days: 90 + if-no-files-found: error + + - name: Upload x86_64 RPM prerelease packages + if: needs.compute-versions.outputs.is_prerelease == 'true' + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: openshell-${{ env.RELEASE_TAG }}-linux-x86_64-rpm + path: | + release/openshell-*.x86_64.rpm + release/openshell-checksums-sha256.txt + retention-days: 90 + if-no-files-found: error + + - name: Upload aarch64 RPM prerelease packages + if: needs.compute-versions.outputs.is_prerelease == 'true' + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: openshell-${{ env.RELEASE_TAG }}-linux-aarch64-rpm + path: | + release/openshell-*.aarch64.rpm + release/openshell-checksums-sha256.txt + retention-days: 90 + if-no-files-found: error + + - name: Upload macOS prerelease packages + if: needs.compute-versions.outputs.is_prerelease == 'true' + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: openshell-${{ env.RELEASE_TAG }}-macos-arm64 + path: | + release/openshell-aarch64-apple-darwin.tar.gz + release/openshell-gateway-aarch64-apple-darwin.tar.gz + release/openshell-driver-vm-aarch64-apple-darwin.tar.gz + release/openshell-prover-aarch64-apple-darwin.tar.gz + release/openshell.rb + release/openshell-checksums-sha256.txt + release/openshell-gateway-checksums-sha256.txt + release/openshell-prover-checksums-sha256.txt retention-days: 90 if-no-files-found: error diff --git a/README.md b/README.md index 3237c169f9..b2eaa651c2 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,9 @@ [![Documentation](https://img.shields.io/badge/docs-latest-brightgreen)](https://docs.nvidia.com/openshell/latest/index.html) [![Project Status](https://img.shields.io/badge/status-alpha-orange)](https://docs.nvidia.com/openshell/latest/about/release-notes.html) +> [!IMPORTANT] +> **OpenShell 0.1.0 is coming soon.** [Track progress in the 0.1.0 milestone](https://github.com/NVIDIA/OpenShell/milestone/10), [read the prerelease documentation](https://docs.nvidia.com/openshell/dev/index.html), or [install a prerelease](#prerelease-and-development-builds). + OpenShell is the safe, private runtime for autonomous AI agents. It provides sandboxed execution environments that protect your data, credentials, and infrastructure — governed by declarative YAML policies that prevent unauthorized file access, data exfiltration, and uncontrolled network activity. OpenShell is built agent-first. It ships public agent skills for using and operating OpenShell, plus separate repository-aware workflows for contributors and maintainers. @@ -27,21 +30,15 @@ OpenShell is built agent-first. It ships public agent skills for using and opera ### Install -**Binary (recommended):** +**Local installation:** ```bash curl -LsSf https://raw.githubusercontent.com/NVIDIA/OpenShell/main/install.sh | sh ``` -The installer installs the latest stable release by default. To install a specific version, set `OPENSHELL_VERSION`. A [`dev` release](https://github.com/NVIDIA/OpenShell/releases/tag/dev) is also available that tracks the latest commit on `main`. - -The `openshell` package on PyPI provides the Python SDK only. It does not install the `openshell` CLI. Add the SDK to a Python project with [uv](https://docs.astral.sh/uv/): - -```bash -uv add openshell -``` +The installer installs the latest stable release by default. See [Prerelease and development builds](#prerelease-and-development-builds) to install an upcoming release or the latest commit on `main`. -**Helm chart:** +**Kubernetes installation:** > **Experimental** — the Kubernetes deployment path is under active development. Expect rough edges and breaking changes. @@ -104,6 +101,44 @@ See the [full walkthrough](examples/sandbox-policy-quickstart/) or run the autom bash examples/sandbox-policy-quickstart/demo.sh ``` +## SDKs + +OpenShell provides client SDKs for Python, TypeScript, Go, and Rust. SDK packages connect applications to an OpenShell gateway; they do not install the `openshell` CLI. Use the SDK and gateway from the same OpenShell release when possible. + +### Python + +The [Python SDK](python/openshell/) is published to [PyPI](https://pypi.org/project/openshell/): + +```shell +uv add openshell +``` + +### TypeScript + +The [TypeScript SDK](sdk/typescript/README.md) is published to GitHub Packages as `@nvidia/openshell-sdk`. Configure the `@nvidia` npm scope for `https://npm.pkg.github.com`, authenticate with a token that has `read:packages`, and install it: + +```shell +npm install @nvidia/openshell-sdk +``` + +### Go + +Add the [Go SDK](sdk/go/README.md) to a Go module: + +```shell +go get github.com/NVIDIA/OpenShell/sdk/go@latest +``` + +### Rust + +The [Rust SDK](crates/openshell-sdk/README.md) is currently consumed from source. Pin the Git dependency to the same OpenShell release as the gateway: + +```shell +cargo add openshell-sdk \ + --git https://github.com/NVIDIA/OpenShell \ + --tag +``` + ## How It Works OpenShell isolates each sandbox in its own container with policy-enforced egress routing. A lightweight gateway coordinates sandbox lifecycle, and every outbound connection is intercepted by the policy engine, which does one of three things: @@ -255,6 +290,43 @@ Agent implementation is human-directed: a user may request a phase directly, or - [Brev Launchable](https://brev.nvidia.com/launchable/deploy/now?launchableID=env-3Ap3tL55zq4a8kew1AuW0FpSLsg) — try OpenShell on cloud compute without local setup - [Agent Instructions](AGENTS.md) — system prompt and workflow documentation for agent contributors +## Prerelease and development builds + +Use a prerelease candidate to evaluate an upcoming release, or use the rolling development build to test the latest commit on `main`. These builds may change before the next stable release. The matching documentation is published in the [development channel](https://docs.nvidia.com/openshell/dev/index.html). + +Prerelease packages are retained as GitHub Actions artifacts for 90 days and require an authenticated [GitHub CLI](https://cli.github.com/) session. The `pre` alias installs the latest prerelease: + +```shell +gh auth login +curl -LsSf https://raw.githubusercontent.com/NVIDIA/OpenShell/main/install.sh | \ + OPENSHELL_VERSION=pre sh +``` + +The installer downloads only the artifact for the current platform and rejects expired candidates during discovery. Installed packages retain the candidate's exact version, such as `0.1.0-pre.3`. Prerelease tags do not create entries on the GitHub Releases page. + +The rolling [`dev` release](https://github.com/NVIDIA/OpenShell/releases/tag/dev) does not require GitHub authentication: + +```shell +curl -LsSf https://raw.githubusercontent.com/NVIDIA/OpenShell/main/install.sh | \ + OPENSHELL_VERSION=dev sh +``` + +For Kubernetes, select the corresponding Helm chart version. Helm chart versions omit the leading `v` from release tags: + +```shell +# Pin an exact candidate +helm upgrade --install openshell \ + oci://ghcr.io/nvidia/openshell/helm-chart \ + --version 0.1.0-pre.3 + +# Rolling development build +helm upgrade --install openshell \ + oci://ghcr.io/nvidia/openshell/helm-chart \ + --version 0.0.0-dev +``` + +Prerelease charts use exact `-pre.N` versions. Development charts are also published as immutable `0.0.0-dev.` versions when you need to pin a specific commit. See the [Helm chart documentation](deploy/helm/openshell/README.md#available-versions) for version and configuration details. + ## Contributing OpenShell is built agent-first. Issues should include a user story, problem statement, impact, and acceptance criteria. The impact should explain the consequences of the current behavior and why existing workarounds are insufficient. Feature requests also require a workflow-level proposed design and alternatives; bug reports add reproduction steps, environment details, and relevant logs. Once work is authorized through the project workflow or a direct request, contributors should use the skills in `.agents/skills/` to investigate the current code and behavior, implement the change, and verify it. If an issue contains earlier diagnostics, verify them rather than relying on them. See [CONTRIBUTING.md](CONTRIBUTING.md) for the full agent skills table, contribution workflow, and development setup. diff --git a/deploy/helm/openshell/README.md b/deploy/helm/openshell/README.md index 231983aca7..003475df96 100644 --- a/deploy/helm/openshell/README.md +++ b/deploy/helm/openshell/README.md @@ -63,10 +63,11 @@ On OpenShift 4.22+, end-to-end TLS is supported via `BackendTLSPolicy`. See the | Tag | Source | Notes | | --- | --- | --- | | `` (e.g. `0.6.0`) | Tagged GitHub release | Tracks the matching gateway, sandbox, and supervisor image versions. Recommended for production. | +| `-pre.N` (e.g. `0.1.0-pre.3`) | A specific prerelease candidate | Immutable candidate pin that tracks images with the same exact version. | | `0.0.0-dev` | Latest commit on `main` | Floating tag, overwritten on every push. `appVersion` is `dev`, so images resolve to the `:dev` tag. | | `0.0.0-dev.` | A specific commit on `main` | Per-commit pin. Chart version and `appVersion` both use the full 40-character commit SHA, which matches the image tag pushed by CI. | -The `dev` tags are intended for testing changes ahead of a release. Production deployments should pin to a tagged release. +Prerelease and `dev` tags are intended for testing changes ahead of a release. Production deployments should pin to a stable tagged release. ## Configuration diff --git a/deploy/helm/openshell/README.md.gotmpl b/deploy/helm/openshell/README.md.gotmpl index 0adac0d070..75e651e214 100644 --- a/deploy/helm/openshell/README.md.gotmpl +++ b/deploy/helm/openshell/README.md.gotmpl @@ -63,10 +63,11 @@ On OpenShift 4.22+, end-to-end TLS is supported via `BackendTLSPolicy`. See the | Tag | Source | Notes | | --- | --- | --- | | `` (e.g. `0.6.0`) | Tagged GitHub release | Tracks the matching gateway, sandbox, and supervisor image versions. Recommended for production. | +| `-pre.N` (e.g. `0.1.0-pre.3`) | A specific prerelease candidate | Immutable candidate pin that tracks images with the same exact version. | | `0.0.0-dev` | Latest commit on `main` | Floating tag, overwritten on every push. `appVersion` is `dev`, so images resolve to the `:dev` tag. | | `0.0.0-dev.` | A specific commit on `main` | Per-commit pin. Chart version and `appVersion` both use the full 40-character commit SHA, which matches the image tag pushed by CI. | -The `dev` tags are intended for testing changes ahead of a release. Production deployments should pin to a tagged release. +Prerelease and `dev` tags are intended for testing changes ahead of a release. Production deployments should pin to a stable tagged release. ## Configuration diff --git a/docs/about/installation.mdx b/docs/about/installation.mdx index 33b4dfc95f..57d8ca6b39 100644 --- a/docs/about/installation.mdx +++ b/docs/about/installation.mdx @@ -20,6 +20,18 @@ The script detects your operating system and installs the OpenShell CLI, standal You can also download release artifacts directly from the [OpenShell GitHub Releases](https://github.com/NVIDIA/OpenShell/releases) page. +### Install a prerelease + +Prerelease packages are retained as GitHub Actions artifacts for 90 days and require an authenticated [GitHub CLI](https://cli.github.com/) session. The `pre` alias installs the latest prerelease: + +```shell +gh auth login +curl -LsSf https://raw.githubusercontent.com/NVIDIA/OpenShell/main/install.sh | \ + OPENSHELL_VERSION=pre sh +``` + +The installer rejects expired candidates, downloads only the artifact required for your platform, and installs with Debian, RPM, or Homebrew. Installed packages retain the candidate's exact version, such as `0.1.0-pre.3`. Prerelease tags do not create entries on the GitHub Releases page. + The `openshell` package on PyPI provides the Python SDK only. It does not install the `openshell` CLI. Add the SDK to a Python project with: ```shell diff --git a/fern/docs.yml b/fern/docs.yml index 65639bd5d0..1161512029 100644 --- a/fern/docs.yml +++ b/fern/docs.yml @@ -59,6 +59,8 @@ versions: - display-name: Latest path: ../docs/index.yml slug: latest + announcement: + message: 'OpenShell 0.1.0 is coming soon. Track progress in the 0.1.0 milestone, read the prerelease documentation, or install a prerelease.' redirects: - source: "/openshell/latest/sandboxes/providers-v2" diff --git a/install.sh b/install.sh index 7f86736148..00f8141135 100755 --- a/install.sh +++ b/install.sh @@ -14,10 +14,15 @@ APP_NAME="openshell" REPO="NVIDIA/OpenShell" GITHUB_URL="https://github.com/${REPO}" RELEASE_TAG="${OPENSHELL_VERSION:-}" +RELEASE_ASSET_DIR="" CHECKSUMS_NAME="openshell-checksums-sha256.txt" LOCAL_GATEWAY_PORT="17670" HOMEBREW_TAP="nvidia/openshell" HOMEBREW_FORMULA_NAME="openshell" +HOMEBREW_CLI_ASSET="openshell-aarch64-apple-darwin.tar.gz" +HOMEBREW_GATEWAY_ASSET="openshell-gateway-aarch64-apple-darwin.tar.gz" +HOMEBREW_DRIVER_VM_ASSET="openshell-driver-vm-aarch64-apple-darwin.tar.gz" +HOMEBREW_PROVER_ASSET="openshell-prover-aarch64-apple-darwin.tar.gz" BREAKING_RELEASE_VERSION="0.0.37" LINUX_PACKAGE_GLIBC_MIN_VERSION="2.28" UPGRADE_NOTICE_ACK="${OPENSHELL_ACK_BREAKING_UPGRADE:-}" @@ -51,6 +56,8 @@ OPTIONS: ENVIRONMENT VARIABLES: OPENSHELL_VERSION Release tag to install (default: latest tagged release). Set OPENSHELL_VERSION=dev to install the rolling dev build. + Set OPENSHELL_VERSION=pre to install the latest prerelease. + Prereleases require an authenticated GitHub CLI session. OPENSHELL_ACK_BREAKING_UPGRADE Set to 1 only after backing up and cleaning up a pre-v0.0.37 installation. @@ -328,6 +335,11 @@ guard_breaking_upgrade() { } resolve_release_tag() { + if [ "${OPENSHELL_VERSION:-}" = "pre" ]; then + resolve_latest_prerelease_tag + return 0 + fi + if [ -n "${OPENSHELL_VERSION:-}" ]; then echo "$OPENSHELL_VERSION" return 0 @@ -355,11 +367,138 @@ resolve_release_tag() { echo "$_version" } +resolve_latest_prerelease_tag() { + require_prerelease_github_access + + info "resolving latest prerelease..." + _artifact_platform="$(prerelease_artifact_platform)" + _successful_run_ids="$(gh api --paginate \ + "repos/${REPO}/actions/workflows/release-tag.yml/runs?status=success&per_page=100" \ + --jq '.workflow_runs[] | select(.status == "completed" and .conclusion == "success") | .id')" || { + error "failed to list successful Release Tag workflow runs" + } + _artifact_records="$(gh api --paginate \ + "repos/${REPO}/actions/artifacts?per_page=100" \ + --jq '.artifacts[] | select(.expired == false) | [.workflow_run.id, .name] | @tsv')" || { + error "failed to list prerelease artifacts" + } + _artifact_names="$(printf '%s\n--ARTIFACTS--\n%s\n' "$_successful_run_ids" "$_artifact_records" | awk -F '\t' ' + $0 == "--ARTIFACTS--" { + reading_artifacts = 1 + next + } + !reading_artifacts { + if ($1 ~ /^[0-9]+$/) successful_runs[$1] = 1 + next + } + $1 in successful_runs { + print $2 + } + ')" + _release_tags="$(printf '%s\n' "$_artifact_names" | sed -n "s/^openshell-\(v[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*-pre\.[1-9][0-9]*\)-${_artifact_platform}$/\1/p" | sort -u)" + + _latest_prerelease="$(printf '%s\n' "$_release_tags" | awk ' + /^v[0-9]+\.[0-9]+\.[0-9]+-pre\.[1-9][0-9]*$/ { + tag = $0 + sub(/^v/, "", tag) + split(tag, version_parts, "-pre[.]") + split(version_parts[1], core, "[.]") + sequence = version_parts[2] + 0 + + if (!found || core[1] + 0 > major || + (core[1] + 0 == major && core[2] + 0 > minor) || + (core[1] + 0 == major && core[2] + 0 == minor && core[3] + 0 > patch) || + (core[1] + 0 == major && core[2] + 0 == minor && core[3] + 0 == patch && sequence > prerelease)) { + selected = $0 + major = core[1] + 0 + minor = core[2] + 0 + patch = core[3] + 0 + prerelease = sequence + found = 1 + } + } + END { + if (found) print selected + } + ')" + + if [ -z "$_latest_prerelease" ]; then + error "no unexpired prerelease artifacts found" + fi + + printf '%s\n' "$_latest_prerelease" +} + +is_prerelease_tag() { + printf '%s\n' "$1" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+-pre\.[1-9][0-9]*$' +} + +require_prerelease_github_access() { + require_cmd gh + if ! gh auth status --hostname github.com >/dev/null 2>&1; then + error "GitHub CLI authentication is required for prerelease artifacts; run 'gh auth login' and try again" + fi +} + +prerelease_artifact_platform() { + case "${PLATFORM}:$(uname -m)" in + linux:x86_64 | linux:amd64) + case "$(linux_package_method)" in + deb) echo "linux-amd64-deb" ;; + rpm) echo "linux-x86_64-rpm" ;; + esac + ;; + linux:aarch64 | linux:arm64) + case "$(linux_package_method)" in + deb) echo "linux-arm64-deb" ;; + rpm) echo "linux-aarch64-rpm" ;; + esac + ;; + darwin:arm64 | darwin:aarch64) echo "macos-arm64" ;; + *) error "no prerelease artifact is available for ${PLATFORM}/$(uname -m)" ;; + esac +} + +prepare_prerelease_assets() { + _destination="$1" + is_prerelease_tag "$RELEASE_TAG" || return 0 + require_prerelease_github_access + + _artifact_name="openshell-${RELEASE_TAG}-$(prerelease_artifact_platform)" + info "locating ${_artifact_name}..." + _run_id="$(gh api \ + "repos/${REPO}/actions/artifacts?name=${_artifact_name}&per_page=100" \ + --jq '[.artifacts[] | select(.expired == false)] | sort_by(.created_at) | last | .workflow_run.id')" || { + error "failed to find prerelease artifact ${_artifact_name}" + } + if [ -z "$_run_id" ] || [ "$_run_id" = "null" ] || ! printf '%s\n' "$_run_id" | grep -Eq '^[0-9]+$'; then + error "no unexpired prerelease artifact found for ${RELEASE_TAG} on this platform" + fi + + RELEASE_ASSET_DIR="${_destination}/release" + info "downloading ${_artifact_name}..." + gh run download "$_run_id" \ + --repo "$REPO" \ + --name "$_artifact_name" \ + --dir "$RELEASE_ASSET_DIR" || { + error "failed to download prerelease artifact ${_artifact_name}" + } +} + download_release_asset() { _tag="$1" _filename="$2" _output="$3" + if [ -n "$RELEASE_ASSET_DIR" ]; then + _source="${RELEASE_ASSET_DIR}/${_filename}" + if [ -f "$_source" ]; then + cp "$_source" "$_output" + return 0 + fi + return 1 + fi + if curl -fLs --retry 3 --max-redirs 5 -o "$_output" \ "${GITHUB_URL}/releases/download/${_tag}/${_filename}"; then return 0 @@ -697,6 +836,28 @@ patch_homebrew_formula() { } +patch_prerelease_homebrew_formula_urls() { + _formula_file="$1" + [ -n "$RELEASE_ASSET_DIR" ] || return 0 + + for _asset in "$HOMEBREW_CLI_ASSET" "$HOMEBREW_GATEWAY_ASSET" "$HOMEBREW_DRIVER_VM_ASSET" "$HOMEBREW_PROVER_ASSET"; do + if [ ! -f "${RELEASE_ASSET_DIR}/${_asset}" ]; then + error "prerelease artifact is missing the required macOS asset: ${_asset}" + fi + done + + _release_asset_url="${GITHUB_URL}/releases/download/${RELEASE_TAG}" + _local_asset_url="file://${RELEASE_ASSET_DIR}" + _patched_file="${_formula_file}.prerelease" + sed \ + -e "s#${_release_asset_url}/${HOMEBREW_CLI_ASSET}#${_local_asset_url}/${HOMEBREW_CLI_ASSET}#g" \ + -e "s#${_release_asset_url}/${HOMEBREW_GATEWAY_ASSET}#${_local_asset_url}/${HOMEBREW_GATEWAY_ASSET}#g" \ + -e "s#${_release_asset_url}/${HOMEBREW_DRIVER_VM_ASSET}#${_local_asset_url}/${HOMEBREW_DRIVER_VM_ASSET}#g" \ + -e "s#${_release_asset_url}/${HOMEBREW_PROVER_ASSET}#${_local_asset_url}/${HOMEBREW_PROVER_ASSET}#g" \ + "$_formula_file" >"$_patched_file" + mv "$_patched_file" "$_formula_file" +} + start_user_gateway() { info "restarting openshell-gateway user service as ${TARGET_USER}..." @@ -891,11 +1052,10 @@ install_linux_deb() { _tmpdir="$(mktemp -d)" chmod 0755 "$_tmpdir" trap 'rm -rf "$_tmpdir"' EXIT - - _checksums_url="${GITHUB_URL}/releases/download/${RELEASE_TAG}/${CHECKSUMS_NAME}" + prepare_prerelease_assets "$_tmpdir" info "downloading ${RELEASE_TAG} release checksums..." - download "$_checksums_url" "${_tmpdir}/${CHECKSUMS_NAME}" || { - error "failed to download ${_checksums_url}" + download_release_asset "$RELEASE_TAG" "$CHECKSUMS_NAME" "${_tmpdir}/${CHECKSUMS_NAME}" || { + error "failed to download ${CHECKSUMS_NAME} for ${RELEASE_TAG}" } _deb_file="$(find_deb_asset "${_tmpdir}/${CHECKSUMS_NAME}" "$_arch")" @@ -903,14 +1063,13 @@ install_linux_deb() { error "no Debian package found for architecture: ${_arch}" fi - _deb_url="${GITHUB_URL}/releases/download/${RELEASE_TAG}/${_deb_file}" _deb_path="${_tmpdir}/${_deb_file}" info "selected ${_deb_file}" info "downloading ${_deb_file}..." download_release_asset "$RELEASE_TAG" "$_deb_file" "$_deb_path" || { - error "failed to download ${_deb_url}" + error "failed to download ${_deb_file} for ${RELEASE_TAG}" } chmod 0644 "$_deb_path" @@ -931,11 +1090,10 @@ install_linux_rpm() { _tmpdir="$(mktemp -d)" chmod 0755 "$_tmpdir" trap 'rm -rf "$_tmpdir"' EXIT - - _checksums_url="${GITHUB_URL}/releases/download/${RELEASE_TAG}/${CHECKSUMS_NAME}" + prepare_prerelease_assets "$_tmpdir" info "downloading ${RELEASE_TAG} release checksums..." - download "$_checksums_url" "${_tmpdir}/${CHECKSUMS_NAME}" || { - error "failed to download ${_checksums_url}" + download_release_asset "$RELEASE_TAG" "$CHECKSUMS_NAME" "${_tmpdir}/${CHECKSUMS_NAME}" || { + error "failed to download ${CHECKSUMS_NAME} for ${RELEASE_TAG}" } _rpm_file="$(find_rpm_asset "${_tmpdir}/${CHECKSUMS_NAME}" "$_arch" openshell)" @@ -961,7 +1119,7 @@ install_linux_rpm() { info "downloading ${_package_file}..." download_release_asset "$RELEASE_TAG" "$_package_file" "$_package_path" || { - error "failed to download ${_package_url}" + error "failed to download ${_package_file} for ${RELEASE_TAG}" } chmod 0644 "$_package_path" @@ -984,7 +1142,7 @@ install_macos_homebrew() { _tmpdir="$(mktemp -d)" chmod 0755 "$_tmpdir" trap 'rm -rf "$_tmpdir"' EXIT - + prepare_prerelease_assets "$_tmpdir" _formula_file="${_tmpdir}/openshell.rb" _formula_url="${GITHUB_URL}/releases/download/${RELEASE_TAG}/openshell.rb" @@ -993,6 +1151,7 @@ install_macos_homebrew() { error "failed to download ${_formula_url}; the selected release may not include a Homebrew formula" } chmod 0644 "$_formula_file" + patch_prerelease_homebrew_formula_urls "$_formula_file" patch_homebrew_formula "$_formula_file" _tap_formula_file="$(homebrew_formula_path "$HOMEBREW_TAP" "$HOMEBREW_FORMULA_NAME")" @@ -1046,8 +1205,8 @@ main() { fi require_cmd curl - RELEASE_TAG="$(resolve_release_tag)" PLATFORM="$(detect_platform)" + RELEASE_TAG="$(resolve_release_tag)" TARGET_USER="$(target_user)" TARGET_UID="$(id -u "$TARGET_USER" 2>/dev/null || true)" diff --git a/python/openshell/release_formula_test.py b/python/openshell/release_formula_test.py index 671577d708..61fff150ac 100644 --- a/python/openshell/release_formula_test.py +++ b/python/openshell/release_formula_test.py @@ -10,7 +10,7 @@ from pathlib import Path -def test_generate_homebrew_formula_uses_tagged_macos_driver_asset_without_default_driver( +def test_generate_homebrew_formula_uses_channel_urls_and_exact_version( tmp_path: Path, ) -> None: release_dir = tmp_path / "release" @@ -42,7 +42,7 @@ def test_generate_homebrew_formula_uses_tagged_macos_driver_asset_without_defaul str(repo_root / "tasks/scripts/release.py"), "generate-homebrew-formula", "--release-tag", - "v0.0.10", + "v0.1.0-pre.3", "--release-dir", str(release_dir), "--output", @@ -54,12 +54,13 @@ def test_generate_homebrew_formula_uses_tagged_macos_driver_asset_without_defaul formula = output.read_text(encoding="utf-8") assert ( "https://github.com/NVIDIA/OpenShell/releases/download/" - "v0.0.10/openshell-driver-vm-aarch64-apple-darwin.tar.gz" + "v0.1.0-pre.3/openshell-driver-vm-aarch64-apple-darwin.tar.gz" ) in formula + assert 'version "0.1.0-pre.3"' in formula assert 'sha256 "' + "b" * 64 + '"' in formula assert ( "https://github.com/NVIDIA/OpenShell/releases/download/" - "v0.0.10/openshell-prover-aarch64-apple-darwin.tar.gz" + "v0.1.0-pre.3/openshell-prover-aarch64-apple-darwin.tar.gz" ) in formula assert 'sha256 "' + "e" * 64 + '"' in formula assert 'resource("openshell-prover").stage' in formula diff --git a/tasks/scripts/test-install-sh.sh b/tasks/scripts/test-install-sh.sh index b9d5f980ff..220fbb43d4 100755 --- a/tasks/scripts/test-install-sh.sh +++ b/tasks/scripts/test-install-sh.sh @@ -121,4 +121,131 @@ if [ "$(find_rpm_asset "${tmpdir}/checksums" x86_64 openshell-prover)" != "opens exit 1 fi +mock_gh_log="${tmpdir}/gh.log" +PLATFORM=linux +linux_package_method() { + printf 'deb\n' +} +uname() { + case "${1:-}" in + -m) printf 'x86_64\n' ;; + *) command uname "$@" ;; + esac +} +gh() { + printf '%s\n' "$*" >>"$mock_gh_log" + case "$1:$2" in + auth:status) + return 0 + ;; + api:*) + case "$*" in + *"?name="*) printf '123456\n' ;; + *"actions/workflows/release-tag.yml/runs?status=success"*) + printf '%s\n' 100 101 + ;; + *) + if [ "${MOCK_NO_PRERELEASE:-0}" != "1" ]; then + printf '%b\n' \ + '100\topenshell-v0.1.0-pre.9-linux-amd64-deb' \ + '101\topenshell-v1.0.0-pre.2-linux-amd64-deb' \ + '101\topenshell-v1.0.0-pre.1-macos-arm64' \ + '101\topenshell-v0.2.0-pre.10-linux-aarch64-rpm' \ + '999\topenshell-v2.0.0-pre.1-linux-amd64-deb' + fi + ;; + esac + ;; + run:download) + while [ "$#" -gt 0 ]; do + if [ "$1" = "--dir" ]; then + shift + mkdir -p "$1" + printf 'checksums\n' >"$1/$CHECKSUMS_NAME" + return 0 + fi + shift + done + return 1 + ;; + *) return 1 ;; + esac +} + +resolved_prerelease="$(OPENSHELL_VERSION=pre resolve_release_tag)" +if [ "$resolved_prerelease" != "v1.0.0-pre.2" ]; then + echo "FAIL: pre alias resolved to ${resolved_prerelease}, expected v1.0.0-pre.2" >&2 + exit 1 +fi +if ! grep -Fq 'actions/workflows/release-tag.yml/runs?status=success' "$mock_gh_log"; then + echo "FAIL: pre alias did not query successful Release Tag workflow runs" >&2 + cat "$mock_gh_log" >&2 + exit 1 +fi +if ! grep -Fq 'select(.status == "completed" and .conclusion == "success")' "$mock_gh_log"; then + echo "FAIL: pre alias did not require completed successful workflow runs" >&2 + cat "$mock_gh_log" >&2 + exit 1 +fi + +if (MOCK_NO_PRERELEASE=1 OPENSHELL_VERSION=pre resolve_release_tag) >"$out" 2>"$err"; then + echo "FAIL: pre alias should fail when no unexpired prerelease artifacts exist" >&2 + exit 1 +fi +if ! grep -Fq 'no unexpired prerelease artifacts found' "$err"; then + echo "FAIL: missing prerelease resolution failure was not explained" >&2 + cat "$err" >&2 + exit 1 +fi + +RELEASE_TAG=v0.1.0-pre.9 +prerelease_tmp="${tmpdir}/prerelease" +prepare_prerelease_assets "$prerelease_tmp" +if [ "$RELEASE_ASSET_DIR" != "${prerelease_tmp}/release" ]; then + echo "FAIL: prerelease artifact directory was not recorded" >&2 + exit 1 +fi +if ! grep -Fq 'select(.expired == false)' "$mock_gh_log"; then + echo "FAIL: prerelease lookup did not filter expired artifacts" >&2 + cat "$mock_gh_log" >&2 + exit 1 +fi +if ! grep -Fq 'actions/artifacts?name=openshell-v0.1.0-pre.9-linux-amd64-deb' "$mock_gh_log"; then + echo "FAIL: prerelease lookup did not select the current platform artifact" >&2 + cat "$mock_gh_log" >&2 + exit 1 +fi +if ! grep -Fq 'run download 123456 --repo NVIDIA/OpenShell --name openshell-v0.1.0-pre.9-linux-amd64-deb' "$mock_gh_log"; then + echo "FAIL: prerelease download did not select the current platform artifact" >&2 + cat "$mock_gh_log" >&2 + exit 1 +fi + +downloaded_checksum="${tmpdir}/downloaded-checksums.txt" +download_release_asset "$RELEASE_TAG" "$CHECKSUMS_NAME" "$downloaded_checksum" +if [ "$(cat "$downloaded_checksum")" != "checksums" ]; then + echo "FAIL: prerelease checksum was not copied from the platform artifact" >&2 + exit 1 +fi + +for asset in "$HOMEBREW_CLI_ASSET" "$HOMEBREW_GATEWAY_ASSET" "$HOMEBREW_DRIVER_VM_ASSET" "$HOMEBREW_PROVER_ASSET"; do + : >"${RELEASE_ASSET_DIR}/${asset}" +done +prerelease_formula="${tmpdir}/openshell.rb" +printf '%s\n' \ + " url \"${GITHUB_URL}/releases/download/${RELEASE_TAG}/${HOMEBREW_CLI_ASSET}\"" \ + " url \"${GITHUB_URL}/releases/download/${RELEASE_TAG}/${HOMEBREW_GATEWAY_ASSET}\"" \ + " url \"${GITHUB_URL}/releases/download/${RELEASE_TAG}/${HOMEBREW_DRIVER_VM_ASSET}\"" \ + " url \"${GITHUB_URL}/releases/download/${RELEASE_TAG}/${HOMEBREW_PROVER_ASSET}\"" \ + >"$prerelease_formula" +patch_prerelease_homebrew_formula_urls "$prerelease_formula" +for asset in "$HOMEBREW_CLI_ASSET" "$HOMEBREW_GATEWAY_ASSET" "$HOMEBREW_DRIVER_VM_ASSET" "$HOMEBREW_PROVER_ASSET"; do + if ! grep -Fq "file://${RELEASE_ASSET_DIR}/${asset}" "$prerelease_formula"; then + echo "FAIL: prerelease formula did not use local asset ${asset}" >&2 + exit 1 + fi +done + +unset -f gh uname linux_package_method + echo "install.sh focused tests passed"