Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ __pycache__/
*.pyc
*.pyo
*.egg-info/
dist/
.coverage
.pytest_cache/
.mypy_cache/
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ All notable changes to Meti are documented here. Versions follow [Semantic Versi
- Release readiness gate: `python scripts/check_release.py` validates version
synchronization, plugin/marketplace metadata, private-path hygiene, wheel
artifact contents, distribution docs, and no-network smoke behavior.
- Versioned release workflow: `release.json`, `scripts/release.py`, complete
GitHub Release artifacts, `scripts/install.sh`, and `meti update` support
latest stable or explicit `--version vX.Y.Z` installs with `--yes` for
reviewed automation.
- Distribution docs for Claude Code local plugin install, future marketplace
install, OpenClaw skill install, and reinstall-first upgrade guidance.
- Marketplace submission packet covering descriptions, tags, install notes,
Expand Down
31 changes: 30 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,22 @@ no-network smoke flow. It is draft-safe and account-free by default: it does not
create live platform drafts, open a browser, read real credentials, or submit
marketplace data.

`release.json` is the canonical publication source for versioned releases.
Use the semi-automatic release workflow so Python package metadata, SKILL
frontmatter, plugin metadata, marketplace metadata, artifacts, changelog, tag,
and GitHub Release stay in sync:

```bash
python scripts/release.py prepare --version X.Y.Z --dry-run
python scripts/release.py build --dry-run
python scripts/release.py publish --version X.Y.Z --dry-run
```

After reviewing the dry-run output, run the same commands without `--dry-run`.
`publish` prints the target version, tag, artifact paths, `SHA256SUMS`, and
`gh release create` command before confirmation. Use `--yes` only in CI or an
already-reviewed scripted release.

## What kind of changes are most welcome

In rough priority order:
Expand Down Expand Up @@ -121,7 +137,7 @@ platform artifacts in tracked files.
- **Commit messages**: imperative present tense, scope-prefixed when it helps (`feat(substack):`, `fix(wechat-image):`, `docs:`, `refactor(browser):`). The recent git log is a good style reference.
- **PRs** target `main`. Squash-merge by default. PR description should answer: what, why, how-verified.
- **Tests required** for any code change. New providers ship with both unit tests and a real-account verification note.
- **Releases**: maintainers tag `vMAJOR.MINOR.PATCH` on `main` after CI green and `python scripts/check_release.py` passes; `pyproject.toml` version bump lands in the same PR as the user-visible feature.
- **Releases**: maintainers publish `vMAJOR.MINOR.PATCH` from `main` after CI green and `python scripts/check_release.py` passes; the `release.json` bump lands in the same PR as the user-visible feature.

### Prefer reinstall

Expand All @@ -135,6 +151,19 @@ only; do not delete `~/.config/meti`, `~/.config/meti/credentials.json.age`, or
Host-native update commands such as `git pull` are acceptable only when the
local install path is known, clean, and verified.

For versioned installs, prefer:

```bash
scripts/install.sh --latest --target ~/.openclaw/skills/meti
scripts/install.sh --version vX.Y.Z --target ~/.openclaw/skills/meti --yes
meti update --latest
meti update --version vX.Y.Z
```

`meti update` confirms interactively by default; `--yes` is for CI or reviewed
automation. Normal reinstall/update preserves `~/.config/meti`,
`~/.config/meti/credentials.json.age`, and `~/.config/meti/age-key.txt`.

## Reporting bugs

Open an issue with:
Expand Down
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,23 @@ The Claude Code marketplace submission is pending. Once Meti is listed, the inst
git clone https://github.com/Nowhitestar/meti.git ~/.openclaw/skills/meti
```

**Versioned release install / update**

```bash
# latest stable from GitHub Releases
scripts/install.sh --latest --target ~/.openclaw/skills/meti

# pinned install or rollback
scripts/install.sh --version vX.Y.Z --target ~/.openclaw/skills/meti --yes

# from an existing checkout/install
meti update --latest
meti update --version vX.Y.Z
```

Normal reinstall/update preserves `~/.config/meti`,
`~/.config/meti/credentials.json.age`, and `~/.config/meti/age-key.txt`.

**Direct CLI** (Python ≥ 3.10)

```bash
Expand Down
86 changes: 86 additions & 0 deletions core/release.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
"""Release manifest helpers for Meti.

This module intentionally avoids provider imports and credential reads. It only
parses the checked-in release manifest used by release tooling.
"""

from __future__ import annotations

import json
import re
from collections.abc import Mapping
from pathlib import Path
from typing import Any

RELEASE_MANIFEST = "release.json"
REQUIRED_ARTIFACT_KINDS = {
"wheel",
"sdist",
"claude-plugin-zip",
"openclaw-skill-zip",
"checksums",
"release-manifest",
}
REQUIRED_HOSTS = {"cli", "claude-plugin", "openclaw-skill"}
STRICT_SEMVER_RE = re.compile(r"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$")


def load_release_manifest(path: str | Path = RELEASE_MANIFEST) -> dict[str, Any]:
"""Load a release manifest from a file or project directory."""

manifest_path = Path(path)
if manifest_path.is_dir():
manifest_path = manifest_path / RELEASE_MANIFEST
return json.loads(manifest_path.read_text(encoding="utf-8"))


def validate_release_manifest(manifest: Mapping[str, Any]) -> list[str]:
"""Return validation errors for the canonical release manifest."""

errors: list[str] = []
version = manifest.get("version")
tag = manifest.get("tag")
channel = manifest.get("channel")
semver_policy = manifest.get("semver_policy")

if not isinstance(version, str) or not STRICT_SEMVER_RE.match(version):
errors.append("version must be strict SemVer X.Y.Z")
if isinstance(version, str) and tag != f"v{version}":
errors.append("tag must equal v<version>")
if channel != "stable":
errors.append("channel must be stable")
if semver_policy != "strict":
errors.append("semver_policy must be strict")

compatibility = manifest.get("compatibility")
if not isinstance(compatibility, dict):
errors.append("compatibility must be an object")
else:
if compatibility.get("python") != ">=3.10":
errors.append("compatibility.python must be >=3.10")
for host in sorted(REQUIRED_HOSTS):
if not isinstance(compatibility.get(host), str) or not compatibility.get(host):
errors.append(f"compatibility.{host} must be set")

artifacts = manifest.get("artifacts")
if not isinstance(artifacts, list) or not artifacts:
errors.append("artifacts must be a non-empty list")
else:
seen: set[str] = set()
for index, artifact in enumerate(artifacts):
if not isinstance(artifact, dict):
errors.append(f"artifacts[{index}] must be an object")
continue
kind = artifact.get("kind")
filename = artifact.get("filename")
if not isinstance(kind, str) or not kind:
errors.append(f"artifacts[{index}].kind must be set")
else:
seen.add(kind)
if not isinstance(filename, str) or not filename:
errors.append(f"artifacts[{index}].filename must be set")
missing = sorted(REQUIRED_ARTIFACT_KINDS - seen)
if missing:
errors.append(f"artifacts missing kinds: {', '.join(missing)}")

return errors
53 changes: 48 additions & 5 deletions docs/distribution.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,37 @@ python3 -m venv .venv
Use a virtualenv by default. It keeps runtime dependencies such as `pyrage` and
`tomli_w` isolated from system Python package-management rules.

## Versioned release install

Versioned installs use GitHub Release artifacts. Default updates target the
latest stable release; use `--version vX.Y.Z` for pinning, rollback, or a
controlled rollout.

```bash
# latest stable
scripts/install.sh --latest --target ~/.openclaw/skills/meti

# explicit version
scripts/install.sh --version vX.Y.Z --target ~/.openclaw/skills/meti --yes
```

The installer downloads the versioned release asset, `release.json`, and
`SHA256SUMS` from GitHub Releases, verifies the selected artifact checksum, and
replaces only the install target files. It does not delete `~/.config/meti`,
`~/.config/meti/credentials.json.age`, or `~/.config/meti/age-key.txt`.

Existing installs can use the CLI wrapper:

```bash
meti update --latest
meti update --version vX.Y.Z
meti update --version vX.Y.Z --yes
```

Without `--yes`, `meti update` prints the current version, target version,
install path, planned `scripts/install.sh` command, and preserved config paths
before asking for confirmation.

## Prefer reinstall

For existing Claude Code plugin or OpenClaw skill installs, Prefer reinstall
Expand Down Expand Up @@ -95,11 +126,23 @@ marketplace submission material:
python scripts/check_release.py
```

The release gate is draft-safe and account-free by default. It checks version
synchronization, plugin and marketplace metadata, tracked private paths,
generated wheel contents, install docs, and the no-network smoke flow. It does
not create live platform drafts, open a browser, read real credentials, submit
marketplace data, or publish anything publicly.
The release gate is draft-safe and account-free by default. It checks
`release.json` version synchronization, plugin and marketplace metadata, tracked
private paths, generated wheel/release bundle contents, install docs, and the
no-network smoke flow. It does not create live platform drafts, open a browser,
read real credentials, submit marketplace data, or publish anything publicly.

Release maintainers build and publish with dry-run-first commands:

```bash
python scripts/release.py prepare --version X.Y.Z --dry-run
python scripts/release.py build --dry-run
python scripts/release.py publish --version X.Y.Z --dry-run
```

Confirmed publish creates the `vX.Y.Z` tag and published GitHub Release only
after the maintainer approves the printed target version, artifact list,
checksums, and `gh release create` command.

## Private artifacts

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ dev = [
# See docs/browser-connectors.md for details.

[tool.setuptools.packages.find]
include = ["core*", "providers*"]
include = ["core*", "providers*", "scripts*"]

[tool.ruff]
line-length = 100
Expand Down
38 changes: 38 additions & 0 deletions release.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"version": "0.4.3",
"tag": "v0.4.3",
"channel": "stable",
"semver_policy": "strict",
"compatibility": {
"python": ">=3.10",
"cli": ">=0.4.3 <1.0.0",
"claude-plugin": ">=0.4.3 <1.0.0",
"openclaw-skill": ">=0.4.3 <1.0.0"
},
"artifacts": [
{
"kind": "wheel",
"filename": "meti-0.4.3-py3-none-any.whl"
},
{
"kind": "sdist",
"filename": "meti-0.4.3.tar.gz"
},
{
"kind": "claude-plugin-zip",
"filename": "meti-claude-plugin-v0.4.3.zip"
},
{
"kind": "openclaw-skill-zip",
"filename": "meti-openclaw-skill-v0.4.3.zip"
},
{
"kind": "checksums",
"filename": "SHA256SUMS"
},
{
"kind": "release-manifest",
"filename": "release.json"
}
]
}
1 change: 1 addition & 0 deletions scripts/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Meti helper scripts packaged for the console entry point."""
Loading
Loading