A Makefile over the npm scripts, and one command that publishes
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) <noreply@anthropic.com>
This commit is contained in:
@@ -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 <target>"
|
||||||
|
@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=<owner/name> 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"
|
||||||
@@ -66,12 +66,46 @@ your launcher, Dock or Start menu — the app does not have to be running to pla
|
|||||||
|
|
||||||
## Development
|
## 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
|
```sh
|
||||||
npm install
|
make release
|
||||||
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
|
The tag comes from `package.json`, so `npm version patch` is the only place a
|
||||||
npm run dist:mac # or dist:win / dist:linux
|
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
|
**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/bootstrap.js` | downloads the engine, the shared core and a config |
|
||||||
| `lib/i18n.js` | the two string tables |
|
| `lib/i18n.js` | the two string tables |
|
||||||
| `renderer/` | plain HTML, CSS and JS — no framework, no build step |
|
| `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
|
`contextIsolation` is on, `nodeIntegration` off, `sandbox` on, and the page
|
||||||
carries a CSP that allows only its own script and stylesheet plus images over
|
carries a CSP that allows only its own script and stylesheet plus images over
|
||||||
|
|||||||
Executable
+96
@@ -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']}\")"
|
||||||
Reference in New Issue
Block a user