195 lines
8.7 KiB
YAML
195 lines
8.7 KiB
YAML
# Build the `omnigent` release distributions (core wheel with the web
|
|
# UI bundled in, plus the `omnigent-client` and `omnigent-ui-sdk` SDK
|
|
# wheels it depends on), run the readiness gates, and publish all three to
|
|
# (Test)PyPI via OIDC Trusted Publishing. The three version-lock together,
|
|
# so every release publishes all three at the same version. Self-contained:
|
|
# publishes straight from this repo on `ubuntu-latest` for clean public
|
|
# PyPI/npm access (never a mirror/proxy).
|
|
#
|
|
# Release flow:
|
|
# 1. Push a version tag (vX.Y.Z / vX.Y.ZrcN) -> build + gate + publish to
|
|
# TestPyPI automatically.
|
|
# 2. Validate the TestPyPI release (install + smoke).
|
|
# 3. Manually dispatch ON THE SAME TAG with destination=pypi -> publish
|
|
# the identical version to PyPI, behind the protected `pypi` env.
|
|
#
|
|
# One-time setup (per index, pypi.org AND test.pypi.org): Trusted Publishers
|
|
# for all three project names pointing at omnigent-ai/omnigent +
|
|
# release-omnigent.yml + the test-pypi/pypi environment (unclaimed names
|
|
# reserved via a pending publisher); GitHub envs test-pypi (unprotected)
|
|
# and pypi (required reviewer). Actions are SHA-pinned per repo convention.
|
|
name: Release omnigent (PyPI)
|
|
|
|
on:
|
|
# PyPI publishing moved to the central secure-release repo; the tag-push
|
|
# trigger is REMOVED so a tag no longer double-publishes. Kept as a manual
|
|
# fallback only, to be deleted once the secure path has done a prod release.
|
|
# Manual run: build + gates always run; destination picks the index, with
|
|
# `pypi` binding the protected environment.
|
|
workflow_dispatch:
|
|
inputs:
|
|
destination:
|
|
description: "Index to publish to."
|
|
type: choice
|
|
default: test-pypi
|
|
options:
|
|
- test-pypi
|
|
- pypi
|
|
# CI-test the workflow on change: build + gates run on the PR; publish
|
|
# steps are condition-gated off PR events.
|
|
pull_request:
|
|
paths:
|
|
- ".github/workflows/release-omnigent.yml"
|
|
|
|
permissions:
|
|
contents: read
|
|
id-token: write # OIDC Trusted Publishing — consumed by the publish steps
|
|
|
|
# Serialize releases on the same ref so two triggers can't race the same tag.
|
|
concurrency:
|
|
group: release-omnigent-${{ github.ref }}
|
|
cancel-in-progress: false
|
|
|
|
jobs:
|
|
build-and-publish:
|
|
# Gated to this repository; inert in forks and mirrors.
|
|
if: github.repository == 'omnigent-ai/omnigent'
|
|
runs-on: ubuntu-latest
|
|
# Bound a hung run under GitHub's 6-hour default (real work takes minutes).
|
|
timeout-minutes: 30
|
|
# Environment binds the Trusted Publisher config: test-pypi unprotected,
|
|
# pypi gated by a required-reviewer rule for real releases.
|
|
environment:
|
|
name: ${{ (github.event_name == 'workflow_dispatch' && inputs.destination == 'pypi') && 'pypi' || 'test-pypi' }}
|
|
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
|
|
- name: Set up uv (clean public resolution, no proxy cache)
|
|
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
|
|
with:
|
|
enable-cache: false
|
|
|
|
- name: Set up Node
|
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
|
with:
|
|
node-version: "20"
|
|
|
|
# 1. Build the web UI FIRST into the package tree, clean. Ordering is
|
|
# load-bearing: the wheel packages on-disk files, so the bundle must
|
|
# exist before `uv build`. `rm -rf` backstops Vite's emptyOutDir
|
|
# against stale bundles; `npm ci` installs the exact locked deps.
|
|
# `--legacy-peer-deps` matches how web's lockfile is generated and
|
|
# validated everywhere else (lint, e2e-ui, web-tests, the regen
|
|
# jobs) — required for the React 19 peer conflict; without it `npm ci`
|
|
# rejects the lockfile ("Missing: yaml@1.10.3 from lock file").
|
|
- name: Build web UI (clean, fresh)
|
|
run: |
|
|
rm -rf omnigent/server/static/web-ui
|
|
npm --prefix web ci --legacy-peer-deps
|
|
npm --prefix web run build # Vite outDir -> omnigent/server/static/web-ui
|
|
|
|
# 2. Tag-driven: the tag must match the version in all three pyprojects
|
|
# and the core package's `==` sibling-SDK pins, so the lockstep
|
|
# contract holds. Skipped on a non-tag ref (nothing to compare).
|
|
- name: Verify tag matches package versions
|
|
if: startsWith(github.ref, 'refs/tags/v')
|
|
run: |
|
|
uv run --no-project python - "${GITHUB_REF_NAME#v}" <<'PY'
|
|
import sys
|
|
import tomllib
|
|
|
|
tag = sys.argv[1]
|
|
# Every package carries the tag's version; every cross-package
|
|
# dep is an exact `==tag` pin (the lockstep contract).
|
|
packages = {
|
|
"pyproject.toml": ("omnigent-client", "omnigent-ui-sdk"),
|
|
"sdks/python-client/pyproject.toml": ("omnigent",),
|
|
"sdks/ui/pyproject.toml": ("omnigent-client",),
|
|
}
|
|
errors = []
|
|
for path, sibling_pins in packages.items():
|
|
with open(path, "rb") as f:
|
|
project = tomllib.load(f)["project"]
|
|
print(f"{path}: version={project['version']}")
|
|
if project["version"] != tag:
|
|
errors.append(f"{path} version {project['version']} does not match tag v{tag}")
|
|
for name in sibling_pins:
|
|
pin = f"{name}=={tag}"
|
|
if pin not in project["dependencies"]:
|
|
errors.append(f"{path} dependencies missing exact pin {pin!r}")
|
|
if errors:
|
|
for e in errors:
|
|
print(f"::error::{e}")
|
|
sys.exit(1)
|
|
PY
|
|
|
|
# 3. Build sdist + wheel for all three into one dist/. The SDKs are
|
|
# path-deps (not a uv workspace), so each needs its own build.
|
|
- name: Build sdists + wheels
|
|
run: |
|
|
uv build --out-dir dist
|
|
uv build sdks/python-client --out-dir dist
|
|
uv build sdks/ui --out-dir dist
|
|
ls -la dist/
|
|
|
|
# 4. Metadata sanity check (long-description renders, fields valid).
|
|
- name: twine check
|
|
run: uvx twine check dist/*
|
|
|
|
# 5. GATE: the UI bundle must be inside the core wheel; fail loud if
|
|
# missing/empty. The glob matches only the core wheel (SDK wheels
|
|
# are omnigent_client-* / omnigent_ui_sdk-*).
|
|
- name: Assert web-UI bundle shipped in the wheel
|
|
run: |
|
|
uv run --no-project python - <<'PY'
|
|
import glob
|
|
import sys
|
|
import zipfile
|
|
|
|
whls = sorted(glob.glob("dist/omnigent-*.whl"))
|
|
if not whls:
|
|
sys.exit("no core omnigent wheel found in dist/")
|
|
whl = whls[-1]
|
|
names = zipfile.ZipFile(whl).namelist()
|
|
ui = [n for n in names if "server/static/web-ui/" in n]
|
|
has_index = any(n.endswith("server/static/web-ui/index.html") for n in ui)
|
|
print(f"{whl}: {len(ui)} web-ui files, index.html present = {has_index}")
|
|
sys.exit(0 if (ui and has_index) else "WEB-UI BUNDLE MISSING FROM WHEEL")
|
|
PY
|
|
|
|
# 6. GATE: the wheels install together and the CLI entry point imports,
|
|
# resolving deps from public PyPI (catches proxy-only deps).
|
|
- name: Smoke-install the built wheels
|
|
run: |
|
|
uv venv --python 3.12 /tmp/omnigent-smoke
|
|
uv pip install --python /tmp/omnigent-smoke/bin/python dist/*.whl
|
|
/tmp/omnigent-smoke/bin/omnigent --version
|
|
|
|
# 7. Persist the built artifacts for inspection.
|
|
- name: Upload built distributions
|
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
with:
|
|
name: dist-omnigent
|
|
path: dist/
|
|
|
|
# PUBLISH via OIDC Trusted Publishing (no token; id-token: write granted
|
|
# above). Attestations stay ON (PEP 740 provenance for a public project).
|
|
- name: Publish to TestPyPI
|
|
# Tag pushes + explicit test-pypi dispatches; PR runs never publish.
|
|
if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && inputs.destination == 'test-pypi')
|
|
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
|
|
with:
|
|
repository-url: https://test.pypi.org/legacy/
|
|
# Let a re-run skip already-landed files after a partial publish;
|
|
# the real-PyPI step omits this so a prod collision fails loud.
|
|
skip-existing: true
|
|
|
|
- name: Publish to PyPI
|
|
# Real PyPI only via deliberate dispatch with destination=pypi,
|
|
# behind the protected `pypi` environment.
|
|
if: github.event_name == 'workflow_dispatch' && inputs.destination == 'pypi'
|
|
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
|
|
# default repository-url is pypi.org
|