A release sends the Cardputer Zero package to M5Stack's app store
installers.yml gains a cardputer-zero job: build-deb.sh in a Debian 13 container on an arm64 runner, the same system as the Cardputer. The release attaches the .deb with the other downloads and lists it in SHA256SUMS, and its notes say where Cardputer users get MeshTerm. github-release.yml gains a store job after the release. It runs scripts/cardputer-zero/store-publish.py, which calls czdev publish with two corrections: the manifest's email is johnputer@meshterm.net, not the GitHub profile's, and the branch name stays the same between czdev's two calls (for a fork, the second call named a branch that was never pushed, and GitHub refused the PR). It also sends czdev's git over HTTPS with the token, and it publishes nothing when the store has the version or a PR for it is open, so a rerun opens no second PR. AppBuilder is pinned to the commit whose functions it corrects. The job needs the CARDPUTER_STORE_TOKEN secret (public_repo scope). The release skill now checks the store PR and counts fifteen assets. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
cc626160d4
commit
c5e7ce5a62
5 files changed
+288
-5
No files matched your search
@@ -10,8 +10,9 @@ point of keeping an `[Unreleased]` section. The one part that takes real work is
|
||||
the entry, because the changelog is prose someone reads, not a list of commit subjects.
|
||||
|
||||
Pushing the tag fires [github-release.yml](../../../.github/workflows/github-release.yml),
|
||||
which calls `installers.yml`, builds all five platforms, attaches them with a combined
|
||||
`SHA256SUMS`, and takes the release notes **from that version's changelog section**. So the
|
||||
which calls `installers.yml`, builds all five platforms and the Cardputer Zero's `.deb`,
|
||||
attaches them with a combined `SHA256SUMS`, sends the `.deb` to M5Stack's app store (the
|
||||
`store` job), and takes the release notes **from that version's changelog section**. So the
|
||||
changelog is not documentation of the release; it *is* the release notes. An empty section
|
||||
publishes "No changelog entry for X.Y.Z."
|
||||
|
||||
@@ -171,6 +172,22 @@ the release itself is fine (it runs after publishing); the usual cause is the
|
||||
`SITE_DEPLOY_KEY` secret, and once that is fixed `gh run rerun <run-id> --failed` finishes
|
||||
it.
|
||||
|
||||
`store` also runs after publishing, so its failure leaves the release fine too. It opens a
|
||||
pull request on `CardputerZero/packages` (from JP's fork), and the store merges an update
|
||||
by itself once its own check passes — no video after the first release. Confirm the PR
|
||||
exists and its check passed:
|
||||
|
||||
```
|
||||
gh pr list -R CardputerZero/packages --author jpmartineau --state all --limit 3
|
||||
```
|
||||
|
||||
If `store` failed, the usual causes are the `CARDPUTER_STORE_TOKEN` secret (missing, or the
|
||||
token revoked) or a change in czdev: the job pins AppBuilder to one commit because
|
||||
`scripts/cardputer-zero/store-publish.py` corrects two of its functions, and the script
|
||||
refuses a czdev without them. A rerun is safe — the script publishes nothing when the store
|
||||
already has the version or a PR for it is open. The manifest's email must be
|
||||
`johnputer@meshterm.net`; the script sets it, so check it once on the PR's files.
|
||||
|
||||
The notes open with an install block the workflow writes from the tag — the same commands
|
||||
as the README, but pinned to *this* release's own assets — and the changelog follows under
|
||||
`## What changed`. **Check that second half actually came out of the changelog**, because a
|
||||
@@ -184,8 +201,9 @@ If that reads "No changelog entry for X.Y.Z.", the extraction failed rather than
|
||||
changelog being empty. Fix the workflow, then repair the published notes in place with
|
||||
`gh release edit vX.Y.Z --notes-file` — the binaries are fine and the tag does not move.
|
||||
|
||||
Fourteen assets is the right count — five version-stamped binaries, five version-less
|
||||
copies of them, `SHA256SUMS`, and the three licence files:
|
||||
Fifteen assets is the right count — five version-stamped binaries, five version-less
|
||||
copies of them, the Cardputer Zero's `meshterm_X.Y.Z-1_arm64.deb`, `SHA256SUMS`, and the
|
||||
three licence files:
|
||||
|
||||
```
|
||||
gh release view vX.Y.Z --json assets --jq '.assets | length'
|
||||
|
||||
@@ -187,6 +187,9 @@ jobs:
|
||||
.\meshterm-@VERSION@-windows-x64.exe
|
||||
```
|
||||
|
||||
**Cardputer Zero** — install MeshTerm from M5Stack's app store on the device. The
|
||||
same package is attached below as `meshterm_@VERSION@-1_arm64.deb`.
|
||||
|
||||
Every binary below is listed in `SHA256SUMS`. The version-less copies —
|
||||
`meshterm-macos-arm64` and its four siblings — are the same binaries under a name
|
||||
that never changes, for anything that wants whichever release is newest without
|
||||
@@ -223,6 +226,52 @@ jobs:
|
||||
$PRERELEASE
|
||||
echo "https://github.com/${{ github.repository }}/releases/tag/$TAG"
|
||||
|
||||
# The Cardputer Zero's package goes to M5Stack's app store. `czdev publish` puts it in a
|
||||
# pull request on CardputerZero/packages, and the store merges an update when its checks
|
||||
# pass. scripts/cardputer-zero/store-publish.py runs czdev with two corrections and stops
|
||||
# if the store already has this version or a pull request for it is open. Thus a second
|
||||
# run of a release opens no second pull request.
|
||||
store:
|
||||
name: send the Cardputer package to the app store
|
||||
needs: publish
|
||||
if: ${{ !inputs.prerelease }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
env:
|
||||
# A token of jpmartineau with the public_repo scope: it uploads to the fork of
|
||||
# CardputerZero/packages and opens the pull request as its author.
|
||||
CARDPUTER_STORE_TOKEN: ${{ secrets.CARDPUTER_STORE_TOKEN }}
|
||||
# The czdev that store-publish.py corrects. Change the two together.
|
||||
APPBUILDER_COMMIT: 64edc2292628b436d3e624b150faeed2d60c31af
|
||||
steps:
|
||||
- name: Check for the token
|
||||
run: |
|
||||
if [ -z "$CARDPUTER_STORE_TOKEN" ]; then
|
||||
echo "::error::CARDPUTER_STORE_TOKEN is not set; scripts/cardputer-zero/store-publish.py says what it needs."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.tag || github.ref }}
|
||||
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: meshterm-cardputer-zero
|
||||
path: deb
|
||||
|
||||
- name: Get czdev
|
||||
run: |
|
||||
git init -q appbuilder
|
||||
git -C appbuilder fetch -q --depth 1 \
|
||||
https://github.com/m5stack/CardputerZero-AppBuilder.git "$APPBUILDER_COMMIT"
|
||||
git -C appbuilder checkout -q FETCH_HEAD
|
||||
|
||||
- name: Publish
|
||||
run: |
|
||||
python3 scripts/cardputer-zero/store-publish.py --czdev appbuilder \
|
||||
--deb deb/meshterm_*_arm64.deb
|
||||
|
||||
website:
|
||||
name: put the version on meshterm.net
|
||||
needs: publish
|
||||
|
||||
@@ -198,3 +198,39 @@ jobs:
|
||||
path: |
|
||||
meshterm-*
|
||||
if-no-files-found: error
|
||||
|
||||
# The Debian package for the Cardputer Zero's app store. It holds compiled code for arm64
|
||||
# and Python 3.13, so it is built in Debian 13 on an arm64 runner: the same system as the
|
||||
# Cardputer. The release attaches it with the binaries above, and its `store` job sends it
|
||||
# to M5Stack's app store.
|
||||
cardputer-zero:
|
||||
name: cardputer-zero-deb
|
||||
runs-on: ubuntu-24.04-arm
|
||||
container: debian:trixie
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
# gcc and the headers compile spidev, which has no arm64 wheel on PyPI.
|
||||
- name: Install the build tools
|
||||
run: |
|
||||
apt-get update
|
||||
apt-get install -y --no-install-recommends \
|
||||
python3 python3-venv python3-dev gcc libc6-dev ca-certificates
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Build
|
||||
run: sh scripts/cardputer-zero/build-deb.sh
|
||||
|
||||
- name: Checksum it
|
||||
working-directory: dist
|
||||
run: |
|
||||
for deb in meshterm_*_arm64.deb; do sha256sum "$deb" > "$deb.sha256"; done
|
||||
cat ./*.sha256
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: meshterm-cardputer-zero
|
||||
path: |
|
||||
dist/meshterm_*_arm64.deb
|
||||
dist/meshterm_*_arm64.deb.sha256
|
||||
if-no-files-found: error
|
||||
+1
-1
@@ -5,7 +5,7 @@ These are tools for the repository. None of them is in the wheel.
|
||||
| Script | What it is for |
|
||||
|---|---|
|
||||
| `basemap-doctor.py` | Finds why a user's map has no basemap: certificates, DNS, proxy, or colour depth |
|
||||
| `cardputer-zero/` | `build-deb.sh` builds the Debian package for M5Stack's app store. Run it on the Cardputer Zero or on another arm64 Debian 13 system. `store/` holds the listing for the store (`app-builder.json`, which `czdev publish` reads), its icon, and its screenshots. `screenshot.py` saves the display of a Cardputer as a 320×170 PNG over SSH, as the store wants a screenshot. `launcher-entry.sh` puts a source checkout in the Cardputer Zero's app launcher. `remove` takes it out. `meshterm.png` is the package's icon: the website's logo, at the launcher's 100×100 size. `meshterm-dev.png` is its negative, for the checkout's entry, so that the two entries look different. `type-text.py` types a pasted string (an API key, a Wi-Fi password) on the Cardputer's keyboard from another computer, over SSH |
|
||||
| `cardputer-zero/` | `build-deb.sh` builds the Debian package for M5Stack's app store. Run it on the Cardputer Zero or on another arm64 Debian 13 system. `store/` holds the listing for the store (`app-builder.json`, which `czdev publish` reads), its icon, and its screenshots. `screenshot.py` saves the display of a Cardputer as a 320×170 PNG over SSH, as the store wants a screenshot. `store-publish.py` sends a package to the store with M5Stack's `czdev`, with two corrections. The release workflow runs it after each release. `launcher-entry.sh` puts a source checkout in the Cardputer Zero's app launcher. `remove` takes it out. `meshterm.png` is the package's icon: the website's logo, at the launcher's 100×100 size. `meshterm-dev.png` is its negative, for the checkout's entry, so that the two entries look different. `type-text.py` types a pasted string (an API key, a Wi-Fi password) on the Cardputer's keyboard from another computer, over SSH |
|
||||
| `picocalc-lyra/` | Setup of the Calculinux device: console font, palette, Wi-Fi. `xiao-radio/` is the kit for the XIAO radio (firmware build, flashing, UART setup) |
|
||||
| `uconsole/` | The uConsole's always-on SPI bridge service. MeshTerm can also control that radio directly, with no script (refer to `docs/devices/uconsole.md`) |
|
||||
|
||||
|
||||
Executable
+180
@@ -0,0 +1,180 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
r"""Publish the Cardputer Zero package to M5Stack's app store, with ``czdev`` and two corrections.
|
||||
|
||||
``czdev publish`` (M5Stack's CardputerZero-AppBuilder) uploads the ``.deb`` to a release in
|
||||
the fork of ``CardputerZero/packages``, pushes a branch with the store listing, and opens a
|
||||
pull request. The store merges an update by itself when its checks pass. This script runs
|
||||
``czdev`` in its own process with two corrections, and it sets up ``git`` for it:
|
||||
|
||||
* The manifest records the email of the uploader. ``czdev`` takes the public email of the
|
||||
GitHub profile. MeshTerm uses only ``johnputer@meshterm.net`` (JP, 2026-10-07), so the
|
||||
script gives ``czdev`` that address.
|
||||
* For a fork, ``czdev`` makes the branch name from the clock two times: before the upload
|
||||
and after it. The upload takes seconds, so the pull request asks for a branch that was
|
||||
never pushed, and GitHub refuses it (HTTP 422). The script makes the second call return
|
||||
the first name.
|
||||
* ``czdev`` pushes to ``git@github.com:``. A CI runner or a Cardputer has no SSH key for
|
||||
GitHub, so the script sends ``git`` over HTTPS with the token, and gives it a name and an
|
||||
email for the commit. These settings are in the environment of this process only.
|
||||
|
||||
The corrections replace two functions of ``czdev``. Thus the release workflow pins
|
||||
AppBuilder to one commit. If a later ``czdev`` does not have these functions, the script
|
||||
stops with an error and does not publish.
|
||||
|
||||
The script publishes nothing when the store already has this version, or when a pull
|
||||
request for it is open. Thus a second run of a release does not open a second pull request.
|
||||
|
||||
python scripts/cardputer-zero/store-publish.py --czdev ~/CardputerZero-AppBuilder \
|
||||
--deb dist/meshterm_0.10.4-1_arm64.deb
|
||||
|
||||
The token comes from ``$CARDPUTER_STORE_TOKEN`` (the release workflow), or from the
|
||||
credentials of ``czdev login`` (a Cardputer). It must have the ``public_repo`` scope.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
#: The only email of MeshTerm.
|
||||
EMAIL = "johnputer@meshterm.net"
|
||||
NAME = "Jean-Pierre Martineau"
|
||||
#: The directory with ``app-builder.json``, the icon, and the screenshots.
|
||||
STORE = Path(__file__).resolve().parent / "store"
|
||||
TARGET = "CardputerZero/packages"
|
||||
PUBLISHED_INDEX = "https://cardputerzero.github.io/packages/dists/stable/main/binary-arm64/Packages"
|
||||
TOKEN_ENV = "CARDPUTER_STORE_TOKEN"
|
||||
|
||||
|
||||
def _api(path: str, token: str) -> object:
|
||||
"""GET one GitHub API path, as JSON."""
|
||||
request = urllib.request.Request(
|
||||
f"https://api.github.com{path}",
|
||||
headers={"Authorization": f"Bearer {token}", "Accept": "application/vnd.github+json"},
|
||||
)
|
||||
with urllib.request.urlopen(request, timeout=30) as response: # noqa: S310 - a fixed https host
|
||||
return json.load(response)
|
||||
|
||||
|
||||
def _deb_field(deb: Path, field: str) -> str:
|
||||
"""One field of the control file of ``deb``."""
|
||||
done = subprocess.run(["dpkg-deb", "-f", str(deb), field], capture_output=True, text=True)
|
||||
return done.stdout.strip()
|
||||
|
||||
|
||||
def _published_versions(package: str) -> set[str]:
|
||||
"""The versions of ``package`` in the store's index. Empty if the index cannot be read."""
|
||||
try:
|
||||
with urllib.request.urlopen(PUBLISHED_INDEX, timeout=30) as response: # noqa: S310
|
||||
text = response.read().decode()
|
||||
except OSError:
|
||||
return set()
|
||||
versions: set[str] = set()
|
||||
for stanza in text.split("\n\n"):
|
||||
fields = dict(line.split(": ", 1) for line in stanza.splitlines() if ": " in line)
|
||||
if fields.get("Package") == package and "Version" in fields:
|
||||
versions.add(fields["Version"])
|
||||
return versions
|
||||
|
||||
|
||||
def _open_pull_request(title: str, login: str, token: str) -> str | None:
|
||||
"""The URL of an open pull request of ``login`` with ``title``, or ``None``."""
|
||||
pulls = _api(f"/repos/{TARGET}/pulls?state=open&per_page=100", token)
|
||||
for pull in pulls:
|
||||
if pull["user"]["login"] == login and pull["title"] == title:
|
||||
return pull["html_url"]
|
||||
return None
|
||||
|
||||
|
||||
def _set_up_git(login: str, token: str) -> None:
|
||||
"""Send ``czdev``'s git over HTTPS with ``token``, as MeshTerm's author."""
|
||||
os.environ["CZDEV_GIT_TOKEN"] = token
|
||||
os.environ["CZDEV_GIT_LOGIN"] = login
|
||||
helper = (
|
||||
'!f() { test "$1" = get && printf "username=%s\\npassword=%s\\n" '
|
||||
'"$CZDEV_GIT_LOGIN" "$CZDEV_GIT_TOKEN"; }; f'
|
||||
)
|
||||
settings = [
|
||||
("url.https://github.com/.insteadOf", "git@github.com:"),
|
||||
("credential.https://github.com.helper", helper),
|
||||
]
|
||||
os.environ["GIT_CONFIG_COUNT"] = str(len(settings))
|
||||
for index, (key, value) in enumerate(settings):
|
||||
os.environ[f"GIT_CONFIG_KEY_{index}"] = key
|
||||
os.environ[f"GIT_CONFIG_VALUE_{index}"] = value
|
||||
os.environ["GIT_TERMINAL_PROMPT"] = "0"
|
||||
for role in ("AUTHOR", "COMMITTER"):
|
||||
os.environ[f"GIT_{role}_NAME"] = NAME
|
||||
os.environ[f"GIT_{role}_EMAIL"] = EMAIL
|
||||
|
||||
|
||||
def _correct(publish: object, client_class: type) -> None:
|
||||
"""Make the two corrections in ``czdev`` (refer to the module docstring)."""
|
||||
get_user = client_class.get_user
|
||||
|
||||
def get_user_with_meshterm_email(self: object): # noqa: ANN202 - czdev's own User
|
||||
user = get_user(self)
|
||||
user.email = EMAIL
|
||||
return user
|
||||
|
||||
client_class.get_user = get_user_with_meshterm_email
|
||||
names: dict[tuple[str, str], str] = {}
|
||||
branch_name = publish.branch_name
|
||||
publish.branch_name = lambda meta: names.setdefault(
|
||||
(meta["package"], meta["version"]), branch_name(meta)
|
||||
)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
"""Check, correct, and run ``czdev publish``."""
|
||||
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
||||
parser.add_argument("--czdev", required=True, type=Path, help="a CardputerZero-AppBuilder")
|
||||
parser.add_argument("--deb", required=True, type=Path, help="the package to publish")
|
||||
args = parser.parse_args()
|
||||
deb = args.deb.resolve()
|
||||
|
||||
sys.path.insert(0, str(args.czdev.resolve() / "scripts"))
|
||||
from czdev import auth, publish # noqa: PLC0415 - from the AppBuilder given on the command line
|
||||
from czdev.github_client import GitHubClient # noqa: PLC0415
|
||||
|
||||
for owner, name in ((publish, "branch_name"), (GitHubClient, "get_user"), (auth, "load_token")):
|
||||
if not callable(getattr(owner, name, None)):
|
||||
sys.stderr.write(
|
||||
f"store-publish: czdev has no {name} -- this czdev is not the pinned one\n"
|
||||
)
|
||||
return 1
|
||||
|
||||
token = os.environ.get(TOKEN_ENV) or auth.load_token()
|
||||
if os.environ.get(TOKEN_ENV):
|
||||
auth.load_token = lambda: token
|
||||
|
||||
package, version = _deb_field(deb, "Package"), _deb_field(deb, "Version")
|
||||
if not package or not version:
|
||||
sys.stderr.write(f"store-publish: {deb} is not a Debian package\n")
|
||||
return 1
|
||||
if version in _published_versions(package):
|
||||
print(f"the store already has {package} {version}: nothing to publish")
|
||||
return 0
|
||||
login = _api("/user", token)["login"]
|
||||
title = f"publish: {package} {version}"
|
||||
already = _open_pull_request(title, login, token)
|
||||
if already:
|
||||
print(f"a pull request for {package} {version} is already open: {already}")
|
||||
return 0
|
||||
|
||||
_correct(publish, GitHubClient)
|
||||
_set_up_git(login, token)
|
||||
# czdev reads app-builder.json from the working directory.
|
||||
os.chdir(STORE)
|
||||
publish.run(deb=str(deb))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in new issue
Block a user