From 85c6d33b0516df39f781be7f64d6e38b32509e75 Mon Sep 17 00:00:00 2001 From: Zsolt Tasnadi Date: Tue, 18 Aug 2026 12:10:41 +0200 Subject: [PATCH] A Makefile over the npm scripts, and one command that publishes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Building and releasing were a build followed by remembered `tea` invocations, and the second half was the part I got wrong by hand: the first release went out with assets attached through a path that half-failed. `make release` is now one command — package for this machine, then create the release and upload — and the pieces are also available separately as `make dist` and `make publish`. The Makefile adds no logic beyond that: every other target wraps an npm script, so `npm start` and `npm run dist:mac` keep working. What it does add is a Node version guard on anything that touches Electron's installer, because a Node 20 `npm install` fails deep inside a postinstall script with an ESM error that says nothing about the cause. `scripts/release.sh` holds the publishing, in POSIX sh like the other repositories' scripts: - the tag comes from package.json, so `npm version patch` is the only place a version is written; - an attachment whose name is already on the release is replaced rather than refused, which is what makes rebuild-and-upload repeatable; - the repository is read from `origin`, so a fork publishes to the fork; - `RELEASE_NOTES.md` becomes the release body when it exists. Tested against the live release with a small probe file rather than by pushing 240 MB twice: creation is skipped when the release exists, a repeat upload takes the replace path, and an empty `dist/` refuses with the command to run instead. The probe was removed afterwards. Co-Authored-By: Claude Opus 5 (1M context) --- Makefile | 88 ++++++++++++++++++++++++++++++++++++++++++ README.md | 47 ++++++++++++++++++++--- scripts/release.sh | 96 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 226 insertions(+), 5 deletions(-) create mode 100644 Makefile create mode 100755 scripts/release.sh diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..936ff08 --- /dev/null +++ b/Makefile @@ -0,0 +1,88 @@ +# WarpEngine Store GUI — the front door to the npm scripts. +# +# Everything here is a thin wrapper: the app is an Electron project, so npm still +# does the work. The Makefile exists so the useful sequences have names, and so +# `make release` is one command rather than a build followed by remembered tea +# invocations. +# +# make list the targets +# make setup install the dependencies +# make dist package for this machine +# make release package and publish to Gitea +# +# Publishing assumes `tea` is installed and logged in — the devarea repo has +# `make tea` for that. + +SHELL := /bin/sh +SCRIPTS := scripts +NODE_MIN := 22 + +# The version is package.json's, so the release tag never drifts from the app. +VERSION := $(shell python3 -c 'import json; print(json.load(open("package.json"))["version"])') +TAG ?= v$(VERSION) + +.DEFAULT_GOAL := help + +.PHONY: help setup node-check start smoke uitest test dist dist-mac dist-win dist-linux \ + release publish clean distclean version + +help: ## List available targets + @echo "WarpEngine Store GUI $(VERSION) — usage: make " + @echo + @grep -E '^[a-zA-Z_-]+:.*?## ' $(MAKEFILE_LIST) | \ + awk 'BEGIN {FS = ":.*?## "}; {printf " %-12s %s\n", $$1, $$2}' + @echo + @echo " Variables: TAG=$(TAG) TEA_LOGIN=ttg REPO= NOTES=RELEASE_NOTES.md" + +node-check: ## Check the Node version Electron's installer needs + @node -e 'const [maj] = process.versions.node.split("."); \ + if (Number(maj) < $(NODE_MIN)) { \ + console.error("Node $(NODE_MIN)+ is needed to install Electron (found " + process.versions.node + \ + "): its installer is ESM-only. The packaged app carries its own runtime."); \ + process.exit(1); \ + } else { console.log("node " + process.versions.node + " ok"); }' + +setup: node-check ## Install the dependencies + npm install + +start: ## Run the app against whatever store is installed + npm start + +smoke: ## Drive the store bridge with no window at all + npm run smoke + +uitest: ## Load the window once and report what rendered + npm run uitest + +test: smoke uitest ## Both checks + +dist: node-check ## Package for this machine + npm run dist + +dist-mac: node-check ## Package for macOS (ad-hoc signed, see the README) + npm run dist:mac + +dist-win: node-check ## Package for Windows + npm run dist:win + +dist-linux: node-check ## Package for Linux + npm run dist:linux + +publish: ## Upload the packages already in dist/ to the Gitea release + @TAG=$(TAG) $(SCRIPTS)/release.sh + +release: dist publish ## Package for this machine and publish it + +clean: ## Remove the built packages + rm -rf dist + +distclean: clean ## Remove the packages and the dependencies + rm -rf node_modules + +version: ## Show the versions involved + @echo "app $(VERSION) (tag $(TAG))" + @printf "node "; node --version 2>/dev/null || echo "missing" + @printf "npm "; npm --version 2>/dev/null || echo "missing" + @printf "electron "; node -p "require('./package.json').devDependencies.electron" 2>/dev/null || echo "missing" + @printf "tea "; tea --version 2>/dev/null | head -1 || echo "missing — devarea: make tea" + @printf "python3 "; python3 --version 2>/dev/null || echo "missing" diff --git a/README.md b/README.md index 57a2d23..1a5c97f 100644 --- a/README.md +++ b/README.md @@ -66,12 +66,46 @@ your launcher, Dock or Start menu — the app does not have to be running to pla ## Development +`make` is the front door; it wraps the npm scripts so the useful sequences have +names. `make` on its own lists everything. + +| Target | What it does | +|---|---| +| `make setup` | install the dependencies (checks the Node version first) | +| `make start` | run the app against whatever store is installed | +| `make smoke` | drive the store bridge with no window at all | +| `make uitest` | load the window once and report what rendered | +| `make test` | both checks | +| `make dist` | package for this machine (`dist-mac`, `dist-win`, `dist-linux` to pick) | +| `make publish` | upload the packages already in `dist/` to the Gitea release | +| `make release` | **package and publish in one go** | +| `make clean` | remove the built packages (`distclean` also drops `node_modules`) | +| `make version` | the versions involved, including whether `tea` is there | + +The npm scripts still work directly (`npm start`, `npm run dist:mac`) — the +Makefile adds no logic of its own beyond the release step. + +### Publishing a release + ```sh -npm install -npm start # the window, against whatever store is installed -npm run smoke # the bridge only: no window, no Electron -npm run uitest # loads the window once and reports what rendered -npm run dist:mac # or dist:win / dist:linux +make release +``` + +The tag comes from `package.json`, so `npm version patch` is the only place a +version is set. The release is created if it is not there yet, and an attachment +whose name is already on it is **replaced** rather than refused — so a rebuild and +a second `make publish` lands rather than erroring. + +Release notes come from `RELEASE_NOTES.md` when the file is present, otherwise the +release gets a one-line note. The repository is read from `origin`, so a fork +publishes to the fork. + +It needs `tea` installed and logged in — the devarea repo has `make tea` for that. +Overridable: `TAG`, `REPO`, `TEA_LOGIN`, `NOTES`, `DIST`. + +```sh +make publish TAG=v1.0.2 # a tag other than package.json's +scripts/release.sh dist/one-file.dmg # just one package ``` **Node 22 or newer is needed to install**, not to run: Electron's own installer @@ -100,6 +134,9 @@ SMOKE_HOME=/tmp/sandbox-root/ttg-desktop npm run smoke | `lib/bootstrap.js` | downloads the engine, the shared core and a config | | `lib/i18n.js` | the two string tables | | `renderer/` | plain HTML, CSS and JS — no framework, no build step | +| `Makefile` | the named sequences; no logic of its own beyond the release | +| `scripts/release.sh` | creates the Gitea release and replaces its attachments | +| `scripts/after-pack.js` | ad-hoc signs the macOS bundle during packaging | `contextIsolation` is on, `nodeIntegration` off, `sandbox` on, and the page carries a CSP that allows only its own script and stylesheet plus images over diff --git a/scripts/release.sh b/scripts/release.sh new file mode 100755 index 0000000..9e8aca8 --- /dev/null +++ b/scripts/release.sh @@ -0,0 +1,96 @@ +#!/bin/sh +# Publish the built packages as a Gitea release. +# +# The version comes from package.json, so the tag follows whatever `npm version` +# set — there is nothing to keep in sync by hand. The release is created if it is +# not there yet, and an attachment with a name already on it is replaced rather +# than refused, which is what makes a rebuild-and-upload repeatable. +# +# scripts/release.sh every package in dist/ +# scripts/release.sh dist/foo.dmg just these +# +# Assumes `tea` is installed and logged in (see the devarea repo: `make tea`). +set -eu + +LOGIN="${TEA_LOGIN:-ttg}" +NOTES="${NOTES:-RELEASE_NOTES.md}" +DIST="${DIST:-dist}" + +say() { echo "[release] $*"; } +die() { echo "[release] error: $*" >&2; exit 1; } + +command -v tea >/dev/null 2>&1 || die "tea is not installed — see the devarea repo, 'make tea'" +command -v python3 >/dev/null 2>&1 || die "python3 is required" +[ -f package.json ] || die "run this from the repository root" + +VERSION="$(python3 -c 'import json; print(json.load(open("package.json"))["version"])')" +TAG="${TAG:-v$VERSION}" + +# The repository is whatever this checkout pushes to, so a fork publishes to the +# fork without editing anything. +REPO="${REPO:-$(git remote get-url origin 2>/dev/null | + sed -e 's#.*[:/]\([^/]*/[^/]*\)$#\1#' -e 's#\.git$##')}" +[ -n "$REPO" ] || die "cannot work out the Gitea repo — set REPO=owner/name" + +# What to upload: the arguments, or every package dist/ holds. +if [ "$#" -gt 0 ]; then + ASSETS="$*" +else + ASSETS="$(find "$DIST" -maxdepth 1 -type f \ + \( -name '*.dmg' -o -name '*-mac.zip' -o -name '*.exe' -o -name '*.AppImage' -o -name '*.deb' \) \ + 2>/dev/null | sort || true)" +fi +[ -n "$ASSETS" ] || die "no packages in $DIST — run 'make dist' first" + +say "$REPO $TAG (version $VERSION), login $LOGIN" + +# --- the release itself ---------------------------------------------------- +if tea api "/repos/$REPO/releases/tags/$TAG" >/dev/null 2>&1; then + say "the release already exists" +else + say "creating the release" + if [ -f "$NOTES" ]; then + tea releases create --login "$LOGIN" --repo "$REPO" --tag "$TAG" \ + --title "$(python3 -c 'import json; d=json.load(open("package.json")); print(f"{d.get(\"productName\") or d[\"name\"]} {d[\"version\"]}")')" \ + --note-file "$NOTES" >/dev/null + else + say "no $NOTES — the release gets a one-line note" + tea releases create --login "$LOGIN" --repo "$REPO" --tag "$TAG" \ + --title "$(python3 -c 'import json; d=json.load(open("package.json")); print(f"{d.get(\"productName\") or d[\"name\"]} {d[\"version\"]}")')" \ + --note "Packages built from $TAG." >/dev/null + fi +fi + +RELEASE_ID="$(tea api "/repos/$REPO/releases/tags/$TAG" | + python3 -c 'import json,sys; print(json.load(sys.stdin)["id"])')" + +# --- the attachments ------------------------------------------------------- +for asset in $ASSETS; do + [ -f "$asset" ] || die "no such file: $asset" + name="$(basename "$asset")" + + # Replacing rather than refusing: a second run after a rebuild should land. + old="$(tea api "/repos/$REPO/releases/$RELEASE_ID/assets" | python3 -c " +import json, sys +name = sys.argv[1] +for a in json.load(sys.stdin): + if a['name'] == name: + print(a['id']) +" "$name")" + for id in $old; do + say "replacing $name" + tea api -X DELETE "/repos/$REPO/releases/$RELEASE_ID/assets/$id" >/dev/null + done + + size="$(python3 -c "import os,sys; print(f'{os.path.getsize(sys.argv[1])/1e6:.0f} MB')" "$asset")" + say "uploading $name ($size) — large packages take a few minutes" + tea releases assets create --login "$LOGIN" --repo "$REPO" "$TAG" "$asset" >/dev/null +done + +say "done:" +tea api "/repos/$REPO/releases/$RELEASE_ID" | python3 -c " +import json, sys +r = json.load(sys.stdin) +for a in r.get('assets') or []: + print(f\" {a['name']} {a['size']/1e6:.0f} MB\") +print(f\" {r['html_url']}\")"