diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 85d0f2b..87569e0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,8 +1,9 @@ name: Release # On main (one squash-merged pull request per release): when pyproject.toml's version has no -# `v` tag yet, tag it and publish a GitHub release with that version's changelog -# section. Projects then pin the tag (zepp-devkit @ git+...@vX.Y.Z). Then reset dev to main +# `` tag yet (or `v`, as releases up to 0.2.0 were tagged), tag it and publish +# a GitHub release with that version's changelog section. Projects then pin the tag +# (zepp-devkit @ git+...@X.Y.Z). Then reset dev to main # (sync-dev), as zepp-kestrel and zepp-watch-faces do. on: @@ -34,11 +35,13 @@ jobs: set -e pip install -q . version=$(python3 -c "import tomllib;print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])") - if git rev-parse -q --verify "refs/tags/v$version" >/dev/null; then - echo "v$version is already released"; exit 0 + # no leading "v" (releases up to 0.2.0 have one) + if git rev-parse -q --verify "refs/tags/$version" >/dev/null || + git rev-parse -q --verify "refs/tags/v$version" >/dev/null; then + echo "$version is already released"; exit 0 fi zepp-devkit notes CHANGELOG.md "$version" > notes.md - gh release create "v$version" --target "$GITHUB_SHA" --title "zepp-devkit $version" --notes-file notes.md + gh release create "$version" --target "$GITHUB_SHA" --title "zepp-devkit $version" --notes-file notes.md # After a release pull request is merged, dev holds the same files as main but different # commits; reset it to main so the next cycle starts clean. Skipped if dev has new work. diff --git a/CHANGELOG.md b/CHANGELOG.md index c0bc510..997c468 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,10 +3,14 @@ All notable changes to the Zepp devkit. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and versions follow [Semantic Versioning](https://semver.org/). Projects pin a commit of a release -(`uses: raetha/zepp-devkit/preview@ # vX.Y.Z`), and Dependabot proposes updates. +(`uses: raetha/zepp-devkit/preview@ # X.Y.Z`), and Dependabot proposes updates. ## [Unreleased] +### Changed + +- Release tags no longer start with "v": a component's default tag is `-` (was `-v`), and this package's own releases are tagged `X.Y.Z` (pin `@0.3.0`, not `@v0.3.0`). A version that already has the older `v` tag counts as released, so nothing is released twice; a project with its own `tag` format gets the same check for that format with a "v" before the version. + ## [0.2.0] - 2026-11-03 ### Added diff --git a/CLAUDE.md b/CLAUDE.md index 4e17141..ca825bc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,7 +2,7 @@ Shared tooling for the owner's Zepp OS projects (raetha/zepp-kestrel, raetha/zepp-watch-faces, more later): GitHub Actions and scripts for building, test builds, Zepp previews, releases and the emulator. Read `README.md` and `docs/plan.md` first. -- **Branches:** Claude runs `dev`: small, settled changes go straight to it, anything else through a `try/` branch and pull request that Claude squash-merges once CI is green; `dev` must stay releasable. Never push to `main`. A release is one squash-merged pull request from `dev` into `main`, opened when the owner asks: it sets `pyproject.toml`'s and `zepp_devkit/__init__.py`'s version and dates the changelog section. After the merge, CI tags `main` as `vX.Y.Z`, publishes the GitHub release and resets `dev` to `main` (`release.yml`). Projects pin a commit with the version in a comment, and Dependabot proposes updates there. +- **Branches:** Claude runs `dev`: small, settled changes go straight to it, anything else through a `try/` branch and pull request that Claude squash-merges once CI is green; `dev` must stay releasable. Never push to `main`. A release is one squash-merged pull request from `dev` into `main`, opened when the owner asks: it dates the changelog section. `dev` holds the expected next version (`pyproject.toml` and `zepp_devkit/__init__.py`): with the first change after a release, set it to what that change calls for under semver, and raise it when a later change calls for more; say what and why in a line. After the merge, CI tags `main` as `X.Y.Z` (no leading "v"; releases up to 0.2.0 have one), publishes the GitHub release and resets `dev` to `main` (`release.yml`). Projects pin a commit with the version in a comment, and Dependabot proposes updates there. - **Repository settings:** a ruleset protects `main` (pull requests, squash only, the `CI passed` check from `ci.yml`'s gate job, CodeQL code scanning results); merged branches are deleted automatically; a classic rule keeps `dev` from being deleted and allows force pushes: after a release squash merge, CI resets `dev` onto `main` (`sync-dev` in `release.yml`) when nothing new landed on `dev` meanwhile; otherwise reset it by hand once that work is released (`git checkout dev && git reset --hard origin/main && git push --force-with-lease`). - Record each change under `## [Unreleased]` in `CHANGELOG.md`, written for the projects using it. Never bump a version or date the changelog without being asked. - **Behaviour changes reach every project.** Keep inputs backwards compatible within a major version; a breaking change is a new major version, with the migration in the changelog. Test with the fake Zeus (`test/`), and then in a project's CI before releasing. diff --git a/README.md b/README.md index 690242a..6a015e3 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ Python code can import it too (`from zepp_devkit.changelog import section, to_ht ]} ``` -Tags are `-v`, or the component's `tag` format. Parts that ship together under one version (a watch app and its Home Assistant integration) are one component: `same_version` lists the other version files, which must match. A component whose draft file exists is never released. Every other component's version needs a changelog section, or `release-plan` fails; with `--dev` (for a `dev` branch that holds the expected next version) an `[Unreleased]` section is enough, so run it without `--dev` on `main` and on pull requests into `main`. +Tags are `-`, or the component's `tag` format; a version that has the older tag with a "v" before the version (`-v`) counts as released too. Parts that ship together under one version (a watch app and its Home Assistant integration) are one component: `same_version` lists the other version files, which must match. A component whose draft file exists is never released. Every other component's version needs a changelog section, or `release-plan` fails; with `--dev` (for a `dev` branch that holds the expected next version) an `[Unreleased]` section is enough, so run it without `--dev` on `main` and on pull requests into `main`. ### In GitHub Actions @@ -105,7 +105,7 @@ Projects that would rather not install it can use the actions, pinned to a commi ### Updating, and trying devkit changes in a project -Projects pin a release tag (`@v0.1.0`). To update, change the pin in `tools/requirements.txt` (Dependabot may propose it). To try a devkit change before it's released, push it to this repository's `dev` and, on a branch of the project, pin that commit (`@`); the project's pull request then runs its CI against it. Projects don't follow this repository's `dev` branch directly: a moving reference would change their builds without a commit of their own, and a broken devkit `dev` would break every project at once. +Projects pin a release tag (`@0.3.0`; releases up to 0.2.0 are tagged `v0.2.0`). To update, change the pin in `tools/requirements.txt` (Dependabot may propose it). To try a devkit change before it's released, push it to this repository's `dev` and, on a branch of the project, pin that commit (`@`); the project's pull request then runs its CI against it. Projects don't follow this repository's `dev` branch directly: a moving reference would change their builds without a commit of their own, and a broken devkit `dev` would break every project at once. ## The Zepp login diff --git a/docs/plan.md b/docs/plan.md index 3691a95..90f8903 100644 --- a/docs/plan.md +++ b/docs/plan.md @@ -17,4 +17,4 @@ How projects use it: one pin in `tools/requirements.txt` (`zepp-devkit @ git+htt Later candidates: Kestrel's model sweep (`tools/compat/`, run the app on every watch model's emulator image) and the faces' screenshot tooling, once a second project needs them. -Each step: build it here with tests, release (`vX.Y.Z`), switch Kestrel (from this session's repo access) and give the owner a prompt for the faces, update both projects' docs to point here, then delete their copies. +Each step: build it here with tests, release (`X.Y.Z`), switch Kestrel (from this session's repo access) and give the owner a prompt for the faces, update both projects' docs to point here, then delete their copies. diff --git a/pyproject.toml b/pyproject.toml index aa1b349..a10d765 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "zepp-devkit" -version = "0.2.0" +version = "0.3.0" description = "Shared build, test-build and publishing tooling for Zepp OS projects" license = "MIT" requires-python = ">=3.10" diff --git a/tests/test_devkit.py b/tests/test_devkit.py index f95119a..e533f78 100644 --- a/tests/test_devkit.py +++ b/tests/test_devkit.py @@ -102,14 +102,14 @@ def test_release_plan_components_and_notes(tmp_path, monkeypatch): releases, problems, lines = release.plan(tmp_path) # twisted's 2.0.0 has no changelog section: that's a problem; sextant is already tagged; ha is a draft assert [r["component"] for r in releases] == ["watch", "twisted"] - assert releases[0] == {"component": "watch", "version": "1.1.0", "tag": "watch-v1.1.0", "title": "Kestrel watch app"} + assert releases[0] == {"component": "watch", "version": "1.1.0", "tag": "watch-1.1.0", "title": "Kestrel watch app"} assert problems == [f"{tmp_path / 'faces/twisted/CHANGELOG.md'} needs a '## [2.0.0]' section (the version is 2.0.0)"] assert "ha: draft, not released" in lines out = tmp_path / "out" monkeypatch.setenv("GITHUB_OUTPUT", str(out)) release.write_github_output(releases) - assert json.loads(out.read_text().split("=", 1)[1])[0]["tag"] == "watch-v1.1.0" + assert json.loads(out.read_text().split("=", 1)[1])[0]["tag"] == "watch-1.1.0" cl = tmp_path / "watch" / "CHANGELOG.md" assert release.notes(cl, "1.0.0", "**Install** it.") == "- First.\n\n---\n\n**Install** it." @@ -132,19 +132,21 @@ def test_release_plan_shared_version_and_dev_mode(tmp_path, monkeypatch): (tmp_path / "CHANGELOG.md").write_text("## [Unreleased]\n\n- x\n\n## [0.1.0] - 2026-11-01\n\n- y\n") (tmp_path / project.CONFIG).write_text(json.dumps({"components": [ {"name": "kestrel", "app": "watch/app.json", "same_version": ["custom_components/kestrel/manifest.json"], - "changelog": "CHANGELOG.md", "tag": "v{version}"}, + "changelog": "CHANGELOG.md", "tag": "{version}"}, ]})) git(tmp_path, "init", "-q") git(tmp_path, "-c", "user.email=a@b", "-c", "user.name=a", "commit", "-q", "--allow-empty", "-m", "x") c = project.component("kestrel", tmp_path) - assert c.tag == "v0.2.0" and c.title == "Kestrel 0.2.0" + assert c.tag == "0.2.0" and c.legacy_tag == "v0.2.0" and c.title == "Kestrel 0.2.0" # dev holds the expected next version: [Unreleased] is enough there, not for a release assert release.plan(tmp_path, dev=True)[1] == [] assert release.plan(tmp_path)[1] == [f"{tmp_path / 'CHANGELOG.md'} needs a '## [0.2.0]' section (the version is 0.2.0)"] (tmp_path / "CHANGELOG.md").write_text("## [0.2.0] - 2026-11-03\n\n- x\n") releases, problems, _ = release.plan(tmp_path) - assert problems == [] and releases[0]["tag"] == "v0.2.0" + assert problems == [] and releases[0]["tag"] == "0.2.0" + git(tmp_path, "tag", "v0.2.0") # released under the older tag form: not released again + assert release.plan(tmp_path)[0] == [] manifest.write_text(json.dumps({"name": "Kestrel", "version": "0.1.9"})) assert release.plan(tmp_path, dev=True)[1] == [ diff --git a/zepp_devkit/__init__.py b/zepp_devkit/__init__.py index fb6f485..46f965d 100644 --- a/zepp_devkit/__init__.py +++ b/zepp_devkit/__init__.py @@ -1,2 +1,2 @@ """Shared tooling for Zepp OS projects. The command line is `zepp-devkit` (cli.py).""" -__version__ = "0.2.0" +__version__ = "0.3.0" diff --git a/zepp_devkit/cli.py b/zepp_devkit/cli.py index d584b6c..a852a47 100644 --- a/zepp_devkit/cli.py +++ b/zepp_devkit/cli.py @@ -133,7 +133,7 @@ def main(argv=None): install = "" if a.install: - fields = {"version": a.version, "name": a.source, "tag": f"{a.source}-v{a.version}"} + fields = {"version": a.version, "name": a.source, "tag": f"{a.source}-{a.version}"} install = Path(a.install).read_text().format(**fields) print(notes(_changelog_path(a.source), a.version, install, a.test_build, a.intro)) elif a.cmd == "extract-zpk": diff --git a/zepp_devkit/project.py b/zepp_devkit/project.py index 5ddf3ac..36fc74f 100644 --- a/zepp_devkit/project.py +++ b/zepp_devkit/project.py @@ -12,8 +12,10 @@ A component's version comes from a Zeus `app.json` (app.version.name, name app.appName) or a Home Assistant `manifest.json` (version, name). With "each", every matching app.json is a component named after its folder ({dir} is that folder, {name} its name). Tags are -`-v`, or the component's "tag" format (e.g. "v{version}"); a component whose -draft file exists is never released. +`-`, or the component's "tag" format (e.g. "{version}"). A version also counts +as released when it has the older tag with a "v" before the version (`-v`, +`v`), so projects that tagged that way don't release a version twice. A component +whose draft file exists is never released. Parts that ship together under one version are one component: its "app" or "manifest" gives the version, and "same_version" lists the other version files (app.json or manifest.json), @@ -22,7 +24,7 @@ {"components": [ {"name": "kestrel", "app": "watch/app.json", "same_version": ["custom_components/kestrel/manifest.json"], - "changelog": "CHANGELOG.md", "draft": "DRAFT", "tag": "v{version}", "title": "{app_name} {version}"} + "changelog": "CHANGELOG.md", "draft": "DRAFT", "tag": "{version}", "title": "{app_name} {version}"} ]} """ import fnmatch @@ -43,9 +45,11 @@ class Component: title: str tag: str = "" others: dict = field(default_factory=dict) # same_version: path -> the version it holds + legacy_tag: str = "" # the tag with a "v" before the version, as earlier releases were tagged def __post_init__(self): - self.tag = self.tag or f"{self.name}-v{self.version}" + self.tag = self.tag or f"{self.name}-{self.version}" + self.legacy_tag = self.legacy_tag or f"{self.name}-v{self.version}" def _version(root, path): @@ -69,7 +73,8 @@ def _read(root, spec, path, name=None, folder=None): changelog=root / fmt("changelog"), draft=bool(draft) and (root / draft).exists(), title=fmt("title", "{app_name} {version}"), - tag=fmt("tag", "{name}-v{version}"), + tag=fmt("tag", "{name}-{version}"), + legacy_tag=str(spec.get("tag", "{name}-{version}")).replace("{version}", "v{version}").format(**fields), others={p: _version(root, p)[0] for p in spec.get("same_version", [])}, ) diff --git a/zepp_devkit/release.py b/zepp_devkit/release.py index 7e64f46..53cb69a 100644 --- a/zepp_devkit/release.py +++ b/zepp_devkit/release.py @@ -1,7 +1,7 @@ """Releases from changelogs: which components need one, and their notes. -A component is released when its version has no tag yet (`-v` unless it sets -its own format). Every non-draft component's current version must have a changelog section, +A component is released when its version has no tag yet (`-` unless it sets +its own format, or the older `-v` form). Every non-draft component's current version must have a changelog section, so a version bump without notes fails CI. With `dev`, for a branch that holds the expected next version, an `[Unreleased]` section is enough: the release pull request names the version's section, and CI on it (and on `main`) runs without `dev`. @@ -29,7 +29,7 @@ def plan(root=".", dev=False): if section(c.changelog, c.version) is None and not (dev and section(c.changelog, "Unreleased") is not None): wanted = f"'## [{c.version}]'" + (" or '## [Unreleased]'" if dev else "") problems.append(f"{c.changelog} needs a {wanted} section (the version is {c.version})") - new = c.tag not in tags + new = c.tag not in tags and c.legacy_tag not in tags if new: releases.append({"component": c.name, "version": c.version, "tag": c.tag, "title": c.title}) lines.append(f"{c.name}: version {c.version} -> {'RELEASE ' + c.tag if new else 'already released'}")