From aec9f9376f5d27bd900285bde5751d54804088e2 Mon Sep 17 00:00:00 2001 From: lyuksovannyy Date: Sat, 12 Sep 2026 23:57:43 +0200 Subject: [PATCH] Publish a build on every push to main Trigger the release workflow on pushes to main, not just on v* tags, so every push leaves a downloadable .plugin attached to the current version. The version in plugin.json decides what happens: a new version creates tag v and a new release, while an unchanged version rebuilds the existing release and replaces its attachment. The tag is never moved, so it keeps pointing at the commit that first introduced that version, but the release notes are refreshed to name the commit the build came from. Hand-pushed v* tags and manual runs are unaffected and still use the tag verbatim. --- .gitea/workflows/release.yml | 15 ++++++++++++--- README.md | 28 ++++++++++++++++------------ tools/release.py | 34 ++++++++++++++++++++++++++++------ 3 files changed, 56 insertions(+), 21 deletions(-) diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index dc3e046..056ac0c 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -2,9 +2,13 @@ name: Publish plugin # Builds dist/.plugin and publishes it to the Releases tab. # -# Triggers: -# push a v* tag -> release named after that tag -# manual dispatch -> release named v +# The version in plugin.json is the single source of truth: +# new version -> a new v tag and release are created +# same version -> the existing release is rebuilt, replacing its attachment +# +# So a normal push to main always produces a downloadable build, and bumping +# the version in plugin.json is what cuts a new release. Pushing a v* tag by +# hand still works and uses that tag verbatim. # # dist/ is gitignored, so the artifact is always built here rather than # committed. The build uses --release because --dev embeds absolute paths from @@ -12,6 +16,8 @@ name: Publish plugin on: push: + branches: + - main tags: - "v*" workflow_dispatch: @@ -54,8 +60,11 @@ jobs: run: | set -eu if [ "${GITHUB_REF_TYPE:-}" = "tag" ]; then + # Hand-pushed tag: honour it exactly as written. tag="${GITHUB_REF_NAME}" else + # Branch push or manual run: the version in plugin.json decides + # whether this is a new release or a rebuild of the current one. tag="v${{ steps.build.outputs.version }}" fi echo "tag=${tag}" >> "$GITHUB_OUTPUT" diff --git a/README.md b/README.md index 8bff6be..f6e51a1 100644 --- a/README.md +++ b/README.md @@ -100,21 +100,25 @@ standard `ANDROID_SERIAL` env var for device selection. ## Publishing a release `.gitea/workflows/release.yml` builds, verifies and publishes the `.plugin` to the repo's -Releases tab. `dist/` is gitignored, so the artifact is always built by CI rather than -committed. +Releases tab on every push to `main`. `dist/` is gitignored, so the artifact is always built +by CI rather than committed. -```bash -# tag a version and push it -> release named after the tag -git tag v1.0.1 && git push origin v1.0.1 -``` +The `version` field in `plugin.json` is the single source of truth: -The workflow also accepts a manual run (Actions → Publish plugin → Run workflow), which -derives the tag from `version` in `plugin.json`. Either way it runs `--check`, builds with -`--release`, and gates the upload on `verify_bundle.py`, so a bundle that fails verification -is never published. +| Push to `main` with… | Result | +| --- | --- | +| a new `version` | creates tag `v` and a new release | +| the same `version` | rebuilds the existing release, replacing its attachment | -Re-running against an existing tag reuses that release and replaces the attachment instead of -piling up duplicates. +So every push leaves a downloadable build attached to the current version, and bumping +`version` is what cuts a new release. Pushing a `v*` tag by hand still works and uses that +tag verbatim, as does a manual run (Actions → Publish plugin → Run workflow). + +Every path runs `--check`, builds with `--release`, and gates the upload on +`verify_bundle.py`, so a bundle that fails verification is never published. A rebuild +replaces the attachment rather than piling up duplicates, and refreshes the release notes to +name the commit it came from. The tag itself is never moved: it keeps pointing at the commit +that first introduced that version. Authentication uses `secrets.API_TOKEN`, an organisation-level Actions secret holding a user token with the `write:repository` scope (which covers releases). Set it once under diff --git a/tools/release.py b/tools/release.py index 98df88f..3ea3278 100755 --- a/tools/release.py +++ b/tools/release.py @@ -67,6 +67,7 @@ def find_release(api: str, tag: str, token: str) -> Optional[dict]: def create_release(api: str, tag: str, token: str, commitish: str, notes: str) -> dict: + """Creates the release, which also creates the tag if it does not exist.""" body = json.dumps( { "tag_name": tag, @@ -83,6 +84,22 @@ def create_release(api: str, tag: str, token: str, commitish: str, notes: str) - return json.loads(payload) +def update_release(api: str, release_id: int, token: str, commitish: str, notes: str) -> dict: + """Refreshes an existing release so a rebuild reports the commit it came from. + + target_commitish is deliberately not changed: the tag already points at the + commit that first introduced this version, and moving it would silently + rewrite history for anyone who already fetched it. + """ + body = json.dumps({"body": notes}).encode() + status, payload = request( + "PATCH", f"{api}/releases/{release_id}", token, body, "application/json" + ) + if status != 200: + fail(f"updating release {release_id} returned HTTP {status}: {payload[:400]!r}") + return json.loads(payload) + + def delete_existing_assets(api: str, release_id: int, name: str, token: str) -> int: """Removes same-named assets, since Gitea keeps duplicates side by side.""" status, payload = request("GET", f"{api}/releases/{release_id}/assets", token) @@ -144,16 +161,21 @@ def main(argv: Optional[list] = None) -> int: api = args.api.rstrip("/") version = json.loads((REPO_ROOT / "plugin.json").read_text(encoding="utf-8"))["version"] + notes = [f"Automated build of version {version}."] + if args.commitish: + notes.append(f"Built from `{args.commitish[:12]}`.") + notes.append("Install by opening the attached `.plugin` file in exteraGram.") + notes_text = "\n\n".join(notes) + release = find_release(api, args.tag, token) if release is None: - notes = ( - f"Automated build of version {version}.\n\n" - "Install by opening the attached `.plugin` file in exteraGram." - ) - release = create_release(api, args.tag, token, args.commitish, notes) + release = create_release(api, args.tag, token, args.commitish, notes_text) print(f"created release {args.tag}") else: - print(f"reusing existing release {args.tag}") + # Version unchanged since the last push: keep the same tag and refresh + # it in place so the attachment always matches the newest commit. + release = update_release(api, int(release["id"]), token, args.commitish, notes_text) + print(f"rebuilding existing release {args.tag}") release_id = int(release["id"]) removed = delete_existing_assets(api, release_id, asset_name, token)