headroom/scripts/release_smoke_all.py
ninosat00 5709291914
chore(release): harden local artifact smokes (#1824)
## Description

Harden the release artifact workflow so maintainers can reproduce npm
and Python release checks locally and so OpenClaw packaging does not
depend on a not-yet-published SDK version. This follow-up also keeps the
OpenClaw loader export contract explicit and updates the PR after the
branch was merged with current `headroomlabs/main`.

## Type of Change

- [x] Bug fix (non-breaking change that fixes an issue)
- [x] New feature (non-breaking change that adds functionality)
- [x] Documentation update

## Changes Made

- Added reusable local release smoke scripts for npm assets, Python
wheel/sdist artifacts, and the combined release gate.
- Switched the release workflow npm asset build to the reusable npm
builder.
- Made OpenClaw release asset building install against the just-built
local SDK tarball before rewriting packed metadata to the release
dependency range.
- Kept the OpenClaw source dependency registry-installable at
`headroom-ai@^0.22.3` while the packed artifact still ships
`^<release-version>`.
- Added `registerHeadroomPlugin` as a named export while preserving the
default `{ register }` OpenClaw loader contract.
- Added Windows development bootstrap docs/script and regression tests
for version sync, npm asset ordering, OpenClaw source installability,
and Python wheel smoke import isolation.

## Testing

- [x] Unit tests pass (`pytest`)
- [x] New tests added for new functionality
- [x] Manual testing performed

### Test Output

```text
uv run --with pytest python -m pytest tests/test_release_workflows.py scripts/tests/test_version_sync.py -q
44 passed, 1 warning

node --check scripts/build_npm_release_assets.mjs
node --check scripts/verify_npm_release_assets.mjs

python -m py_compile scripts/build_python_release_smoke.py scripts/release_smoke_all.py scripts/version-sync.py
python scripts/verify-versions.py
All versions aligned at 0.31.0

npm ci (plugins/openclaw)
added 88 packages, audited 89 packages, found 0 vulnerabilities

python scripts/release_smoke_all.py --out release-assets-local/all-pr1824-postmerge-fixed-20260710-104739
Verified npm release assets for 0.31.0
wheel metadata OK: headroom_ai-0.31.0-cp310-abi3-win_amd64.whl contains headroom/_core.pyd
sdist License-File metadata OK: ['LICENSE', 'NOTICE']
smoke-import OK: version=0.31.0 hello=headroom-core
```

## Real Behavior Proof

- Environment: Windows 11, Python 3.14.3, Node v24.14.0, npm 11.9.0, uv
0.11.15, Rust/Cargo already installed in the local development
environment.
- Exact command / steps: Ran `python scripts/release_smoke_all.py --out
release-assets-local/all-pr1824-postmerge-fixed-20260710-104739` after
syncing this branch with current `headroomlabs/main`.
- Observed result: The command built `headroom-ai-0.31.0.tgz`,
`headroom-openclaw-0.31.0.tgz`,
`headroom_ai-0.31.0-cp310-abi3-win_amd64.whl`, and
`headroom_ai-0.31.0.tar.gz`; npm verifier passed; the wheel installed
into a fresh venv and imported `headroom._core`.
- Not tested: Full GitHub Actions release matrix and publish jobs with
real PyPI/npm/GitHub Packages credentials.

## Review Readiness

- [x] I have performed a self-review
- [x] This PR is ready for human review

## Checklist

- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [x] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] I have updated the CHANGELOG.md if applicable

## Screenshots (if applicable)

N/A.

## Additional Notes

The post-merge full smoke initially failed because the OpenClaw source
package depended on `headroom-ai@^0.31.0`, which is not available on
public npm yet. The builder now installs OpenClaw against the just-built
local SDK tarball before build/pack, and then rewrites packed metadata
to `^0.31.0`.

---------

Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
Co-authored-by: Tejas Chopra <chopratejas@gmail.com>
2026-07-14 16:07:34 -04:00

109 lines
3.3 KiB
Python

#!/usr/bin/env python
"""Run the local release artifact smoke suite.
This command is the one-stop local release gate for artifact packaging. It runs
the version preflight, then delegates to the npm and Python artifact smoke
builders so their detailed checks stay in one place.
"""
from __future__ import annotations
import argparse
import os
import re
import shutil
import subprocess
import sys
from datetime import datetime
from pathlib import Path
try:
import tomllib
except ModuleNotFoundError: # pragma: no cover - Python 3.10 fallback
import tomli as tomllib # type: ignore[no-redef]
ROOT = Path(__file__).resolve().parents[1]
PYPROJECT = ROOT / "pyproject.toml"
def load_project_version() -> str:
with PYPROJECT.open("rb") as fh:
return tomllib.load(fh)["project"]["version"]
def quote_arg(value: str | os.PathLike[str]) -> str:
text = str(value)
if re.fullmatch(r"[A-Za-z0-9_./:=\\-]+", text):
return text
return f'"{text.replace(chr(34), chr(34) * 2)}"'
def run(args: list[str | os.PathLike[str]]) -> None:
print("\n> " + " ".join(quote_arg(arg) for arg in args), flush=True)
subprocess.run([str(arg) for arg in args], cwd=ROOT, check=True)
def ensure_empty_dir(path: Path) -> None:
path.mkdir(parents=True, exist_ok=True)
if any(path.iterdir()):
raise SystemExit(f"output directory must be empty to avoid stale artifacts: {path}")
def parse_args() -> argparse.Namespace:
version = load_project_version()
stamp = datetime.now().strftime("%Y%m%d-%H%M%S")
default_out = ROOT / "release-assets-local" / f"all-{version}-{stamp}"
parser = argparse.ArgumentParser(description="Run local npm and Python release smokes.")
parser.add_argument("--out", type=Path, default=default_out)
parser.add_argument("--python", default=sys.executable)
parser.add_argument("--node", default=shutil.which("node") or "node")
parser.add_argument(
"--python-release",
action="store_true",
help="Run the Python smoke with maturin --release instead of the faster ci profile.",
)
parser.add_argument("--skip-npm", action="store_true")
parser.add_argument("--skip-python", action="store_true")
return parser.parse_args()
def main() -> None:
args = parse_args()
out_dir = args.out.resolve()
version = load_project_version()
if args.skip_npm and args.skip_python:
raise SystemExit("nothing to run: --skip-npm and --skip-python were both set")
ensure_empty_dir(out_dir)
run([args.python, "scripts/verify-versions.py"])
completed: list[tuple[str, Path]] = []
if not args.skip_npm:
npm_out = out_dir / "npm"
run([args.node, "scripts/build_npm_release_assets.mjs", version, npm_out])
completed.append(("npm", npm_out))
if not args.skip_python:
python_out = out_dir / "python"
python_args: list[str | os.PathLike[str]] = [
args.python,
"scripts/build_python_release_smoke.py",
"--out",
python_out,
]
if args.python_release:
python_args.append("--release")
run(python_args)
completed.append(("python", python_out))
print("\nLocal release smoke suite complete:")
for name, path in completed:
print(f"- {name}: {path}")
if __name__ == "__main__":
main()