← Commit history

feat(release): add tools/release.sh, --version for ashvm/kiln/apm, MIT license, beta-v0.1.0 changelog

lennoxrose committed on Oct 19, 2026 at 02:40 UTC

1307bc7parent85d6a7e

12 files changed+305−5

.gitignoremodified

+2−0
@@ -1,6 +1,7 @@
11 # Build artifacts
22 *.o
33 /bin/
4+/dist/
45
56 # apm-installed dependencies (see ideas/ash_modules.md) -- fetched,
67 # never hand-edited, same spirit as node_modules/
@@ -34,6 +35,7 @@ AGENTS.md
3435 .agents
3536 skills-lock.json
3637 docs/*
38+!docs/RELEASING.md
3739 spec/
3840
3941 # Private Notes

CHANGELOG.mdmodified

+7−0
@@ -1,3 +1,10 @@
1+## 2026-10-19 (beta-v0.1.0: first beta release)
2+- **First packaged release.** One tarball per platform, `ash-<os>-<arch>.tar.gz` (today `ash-linux-x86_64.tar.gz`), unpacking into `~/.ash` as `bin/ashvm`, `bin/kiln`, `bin/apm` and a `VERSION` file, plus `SHA256SUMS`. Cut with `make release VERSION=<tag>` / `tools/release.sh` (see `docs/RELEASING.md`).
3+- **What is in it.** `ashvm` (bytecode VM, REPL, `run`, `check`), `kiln` (native Linux ELF compiler; `--target=windows` writes a PE, not tested on a real Windows machine for this release), `apm` (`new`, `check`, `init`, `add`, `install`, `list`, `license`, `run`), all on the one shared front end, with the ownership / memory-safety pass (`memory-safety` defaults to `on`, mnemo is linked in statically; no Rust toolchain is needed to run any of it).
4+- **`--version`** for `ashvm`, `kiln` and `apm` prints the release tag (`dev` in a plain `make`); injected at build time with `make ASH_VERSION=<tag>`. Nothing else in the tools changed.
5+- **Release link flags.** `make ASH_STATIC_RT=1` links libgcc and libstdc++ into `ashvm` statically, so the release `ashvm` needs only libc, libm, libgomp (`libgomp.so.1`) and the Vulkan loader (`libvulkan.so.1`). The default build is unchanged.
6+- **Not in this release.** The standard library is the separate `ash-stdlib` package (`apm add github:<user>/ash-stdlib`). `kiln --link=shared` needs `libkilnrt.so` installed under `/usr/local/lib/kiln`, so it is not part of the tarball (static builds, the default, need nothing). No macOS, aarch64 or Windows tarball.
7+
18 ## 2026-10-18 (Mnemosyne: mnemo.a, freeing sole-owner values)
29 - **`bin/lib/mnemo.a`** (`hearth/mnemo`, Rust, C ABI in `mnemo/H/flow/flow.h`) is linked by both `ashvm` and `kiln`. It holds the move-flow engine of the ownership pass: moved sets, dead paths and the snapshots/joins at branches and loops. `front/src/jown` walks the typed AST and reports events to it. Built by `hearth/mnemo/Makefile` (cargo; a missing toolchain is a hard error pointing at `tools/install.sh`), wired into the `ashvm` and `kiln` Makefiles. C-ABI regression: `mnemo/tests/scripts/run_regression.sh`.
310 - **Freeing, `memory-safety: on` projects, `ashvm` only.** `jown` proves which locals are the sole owner of a fresh String (non-constant `+`) or array (`new T[n]`, `{..}`) and are only read (index, `.length`, operands, String methods returning primitives, print args, for-each). Lowering frees those with the new builtin `mem_free(x)` at the end of the block, before `return`/`break`/`continue` (the return value is computed first), and before a reassignment (`s += x`). Concatenation temporaries (an inner `a + b`, `str(int)`) are freed by `mem_cat(a, b, flags)`; `new T[n]` uses `__newarr1` so no dims array leaks. Everything else is left alone: leaking is safe, freeing a shared value is not. `kiln` lowers none of this yet (`jl_drops`), so its IR is unchanged until it has a free-list allocator.

LICENSEadded

+21−0
@@ -0,0 +1,21 @@
1+MIT License
2+
3+Copyright (c) 2026 lennoxrose
4+
5+Permission is hereby granted, free of charge, to any person obtaining a copy
6+of this software and associated documentation files (the "Software"), to deal
7+in the Software without restriction, including without limitation the rights
8+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+copies of the Software, and to permit persons to whom the Software is
10+furnished to do so, subject to the following conditions:
11+
12+The above copyright notice and this permission notice shall be included in all
13+copies or substantial portions of the Software.
14+
15+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+SOFTWARE.

Makefilemodified

+6−1
@@ -7,7 +7,7 @@
77 HEARTH := hearth
88 SUBDIRS := $(patsubst %/Makefile,%,$(wildcard $(HEARTH)/*/Makefile))
99
10-.PHONY: all clean $(SUBDIRS) $(addprefix clean-,$(SUBDIRS))
10+.PHONY: all clean release $(SUBDIRS) $(addprefix clean-,$(SUBDIRS))
1111
1212 all: clean-bin $(SUBDIRS)
1313
@@ -24,6 +24,11 @@ clean: $(addprefix clean-,$(SUBDIRS))
2424 $(addprefix clean-,$(SUBDIRS)):
2525 $(MAKE) -C $(patsubst clean-%,%,$@) clean
2626
27+# `make release VERSION=beta-v0.1.0 [ARGS=--dry-run]`: see docs/RELEASING.md
28+release:
29+ @test -n "$(VERSION)" || { echo "usage: make release VERSION=<tag> [ARGS='--dry-run --yes']" >&2; exit 2; }
30+ tools/release.sh $(VERSION) $(ARGS)
31+
2732 @%:
2833 @if [ ! -f $(HEARTH)/$*/Makefile ]; then \
2934 echo "no such subproject: $*" >&2; \

docs/RELEASING.mdadded

+61−0
@@ -0,0 +1,61 @@
1+# Releasing Ash
2+
3+One command cuts a release:
4+
5+```sh
6+make release VERSION=beta-v0.2.0 # builds, tests, packages, asks, then publishes
7+make release VERSION=beta-v0.2.0 ARGS=--dry-run # everything except tag / push / publish
8+tools/release.sh beta-v0.2.0 --yes # same thing without `make`, no confirmation prompt
9+```
10+
11+Tags are `beta-vX.Y.Z` or `vX.Y.Z`. The release is published as the **latest** release and never as a
12+pre-release (GitHub's `/releases/latest/download/...` ignores pre-releases); "beta" lives in the tag, the
13+title (`Ash <tag>`) and the notes.
14+
15+## Prerequisites
16+
17+- On `main`, clean tree, equal to `origin/main` (push first). A dry run only reports this.
18+- A `## YYYY-MM-DD (<tag> ...)` section at the top of `CHANGELOG.md`. It becomes the release notes; the
19+ tool stops if it is missing.
20+- `git`, `gh` (logged in: `gh auth login`), `make`, `gcc`, `cargo` (mnemo; `tools/install.sh`), GNU `tar`,
21+ `sha256sum`, `curl`, `bwrap` (bubblewrap, the smoke-test sandbox).
22+- A checkout of `ash-stdlib` next to this repo (the test scripts read `../ash-stdlib/src/main/ash`) and
23+ network access (the `apm` smoke test clones from GitHub).
24+
25+## What the tool does
26+
27+1. Checks the tag format and that the tag exists neither locally nor on origin; checks git state and `gh`.
28+2. `make clean && make` with `ASH_VERSION=<tag>` and `ASH_STATIC_RT=1` exported to every nested make.
29+3. Runs the mnemo, type-check, ashvm, kiln and apm suites. Any failure aborts.
30+4. Builds `dist/ash-<os>-<arch>.tar.gz` (`bin/ashvm`, `bin/kiln`, `bin/apm`, `VERSION`; stripped copies,
31+ nothing wrapped in a folder) and `dist/SHA256SUMS`.
32+5. Smoke test: extracts the tarball and runs it in a `bwrap` sandbox that contains only the shared
33+ libraries `ldd` lists for the three binaries (no compiler, no cargo, no PATH, no network): `--version`
34+ of each tool, `ashvm run` and `kiln` + run on `examples/HelloWorld.ash`, `apm new`.
35+6. `sha256sum -c`, then a summary. Without `--dry-run` it asks before tagging (unless `--yes`).
36+7. Annotated tag, `git push origin <tag>`, `gh release create <tag> dist/* --latest`.
37+8. Downloads `SHA256SUMS` and the tarball from `.../releases/latest/download/` and verifies them.
38+
39+Install location is `~/.ash`: extracting a tarball there gives `~/.ash/bin/ashvm` etc. Nothing else is
40+needed at run time: `kiln` writes static executables and finds no files of its own.
41+
42+## Adding a platform
43+
44+The tool builds and tests the machine it runs on, so a platform needs a machine of that kind.
45+
46+1. On that machine run the tool; add the platform name (`<os>-<arch>`, `uname -s` lowercased and
47+ `uname -m` normalised to `x86_64` / `aarch64`) to `SUPPORTED` in `tools/release.sh` once it passes
48+ the whole flow, including the smoke test.
49+2. Its tarball must be named `ash-<os>-<arch>.tar.gz` with the same layout. Attach it to the existing
50+ release with `gh release upload <tag> ash-<os>-<arch>.tar.gz`, then regenerate `SHA256SUMS` for all
51+ tarballs and upload it with `--clobber`. Do not ship a platform that was not built and run.
52+
53+## Fixing a bad release
54+
55+Never move or reuse a tag. Delete the release and the tag, then release the next version:
56+
57+```sh
58+gh release delete <tag> --cleanup-tag --yes # removes the release and the remote tag
59+git tag -d <tag> # the local tag
60+make release VERSION=<next tag> # with a CHANGELOG section for the next tag
61+```

hearth/apm/Makefilemodified

+3−1
@@ -1,6 +1,8 @@
11 CC = gcc
22 .DEFAULT_GOAL := all
3-CFLAGS = -Wall -Wextra -std=c11 -O2 -Isrc
3+# make ASH_VERSION=<tag> bakes the release tag into --version (tools/release.sh does this)
4+ASH_VERSION ?= dev
5+CFLAGS = -DASH_VERSION='"$(ASH_VERSION)"' -Wall -Wextra -std=c11 -O2 -Isrc
46
57 APM_SRC = \
68 src/C/main.c \

hearth/apm/src/C/main.cmodified

+5−0
@@ -3,6 +3,10 @@
33 #include "H/cmd.h"
44 #include "H/ui.h"
55
6+#ifndef ASH_VERSION
7+#define ASH_VERSION "dev"
8+#endif
9+
610 static void print_usage(void) {
711 printf("apm -- the Ash package manager (github: sources only, for now)\n\n");
812 printf("usage:\n");
@@ -19,6 +23,7 @@ static void print_usage(void) {
1923 int main(int argc, char **argv) {
2024 if (argc < 2) { print_usage(); return 1; }
2125
26+ if (argc == 2 && strcmp(argv[1], "--version") == 0) { printf("apm %s\n", ASH_VERSION); return 0; }
2227 const char *command = argv[1];
2328 int rest_argc = argc - 2;
2429 char **rest_argv = argv + 2;

hearth/ashvm/Makefilemodified

+13−2
@@ -24,8 +24,19 @@ GC ?= 0
2424 ifeq ($(GC),1)
2525 GC_FLAGS = -DASH_GC
2626 endif
27-CFLAGS = -Wall -Wextra -std=c11 -O2 -Isrc -I../front/src -I../pyre/src -fopenmp $(GC_FLAGS)
28-LDFLAGS = -lm ../../bin/lib/front.a ../../bin/lib/mnemo.a ../../bin/extensions/pyre/libpyre.a -fopenmp -ldl -lvulkan -lstdc++ $(CUDA_LIBS)
27+# make ASH_VERSION=<tag> bakes the release tag into --version (tools/release.sh does this)
28+ASH_VERSION ?= dev
29+CFLAGS = -DASH_VERSION='"$(ASH_VERSION)"' -Wall -Wextra -std=c11 -O2 -Isrc -I../front/src -I../pyre/src -fopenmp $(GC_FLAGS)
30+# `make ASH_STATIC_RT=1` (used by tools/release.sh) links libgcc and libstdc++ statically, so a release binary
31+# needs only libc, libm, libgomp and the Vulkan loader on the target machine (libgomp has no static library
32+# on every distro). Same code, only the link differs.
33+ASH_STATIC_RT ?= 0
34+ifeq ($(ASH_STATIC_RT),1)
35+STDCXX = -static-libgcc -Wl,-Bstatic -lstdc++ -Wl,-Bdynamic
36+else
37+STDCXX = -lstdc++
38+endif
39+LDFLAGS = -lm ../../bin/lib/front.a ../../bin/lib/mnemo.a ../../bin/extensions/pyre/libpyre.a -fopenmp -ldl -lvulkan $(STDCXX) $(CUDA_LIBS)
2940
3041 ASHVM_SRC = \
3142 src/app/C/main.c \

hearth/ashvm/src/app/C/main.cmodified

+5−0
@@ -8,6 +8,10 @@
88 #include "jdriver/H/jdriver.h"
99 #include "jlower/H/jlower.h"
1010
11+#ifndef ASH_VERSION
12+#define ASH_VERSION "dev"
13+#endif
14+
1115 void ashvm_execute_ir(char *ir) {
1216 diagnostics_set_source(ir, "<internal ir>");
1317 vm_run(compile(ir));
@@ -21,6 +25,7 @@ static int usage(const char *prog) {
2125 }
2226
2327 int main(int argc, char *argv[]) {
28+ if (argc == 2 && strcmp(argv[1], "--version") == 0) { printf("ashvm %s\n", ASH_VERSION); return 0; }
2429 if (argc < 2) return repl_run();
2530
2631 if (strcmp(argv[1], "run") == 0) {

hearth/kiln/Makefilemodified

+3−1
@@ -1,7 +1,9 @@
11 CC = gcc
22 .DEFAULT_GOAL := all
33 KILN_RPATH_LIST = $(abspath ../../bin/extensions/pyre):/usr/local/lib/kiln
4-CFLAGS = -Wall -Wextra -std=c11 -O2 -Isrc -I../front/src -DKILN_RPATH='"$(KILN_RPATH_LIST)"'
4+# make ASH_VERSION=<tag> bakes the release tag into --version (tools/release.sh does this)
5+ASH_VERSION ?= dev
6+CFLAGS = -DASH_VERSION='"$(ASH_VERSION)"' -Wall -Wextra -std=c11 -O2 -Isrc -I../front/src -DKILN_RPATH='"$(KILN_RPATH_LIST)"'
57 LDFLAGS = ../../bin/lib/front.a ../../bin/lib/mnemo.a -lpthread -ldl -lm
68
79 KILN_SRC = \

hearth/kiln/src/app/C/main.cmodified

+5−0
@@ -14,6 +14,10 @@
1414 #include "codegen/H/runtime/heap.h"
1515 #include "app/H/target.h"
1616
17+#ifndef ASH_VERSION
18+#define ASH_VERSION "dev"
19+#endif
20+
1721 static void usage(const char *prog) {
1822 fprintf(stderr, "usage: %s <file.ash> -o <output> [--target=linux|windows] [--link=static|shared] [--wrap-int]\n", prog);
1923 fprintf(stderr, " %s --emit-runtime -o <libkilnrt.so|libkilnrt.dll> [--target=linux|windows]\n", prog);
@@ -37,6 +41,7 @@ static KilnLinkMode parse_link(const char *arg, const char *prog) {
3741 }
3842
3943 int main(int argc, char *argv[]) {
44+ if (argc == 2 && strcmp(argv[1], "--version") == 0) { printf("kiln %s\n", ASH_VERSION); return 0; }
4045 // --emit-runtime -o <path> [--target=...]: writes libkilnrt.so/.dll
4146 // (Plan B) and exits, no .ash input at all -- an entirely separate
4247 // mode from compiling a program.

tools/release.shadded

+174−0
@@ -0,0 +1,174 @@
1+#!/bin/sh
2+# Cut an Ash release: build, test, package, smoke test, then tag and publish on GitHub.
3+#
4+# tools/release.sh <tag> [--dry-run] [--yes]
5+#
6+# <tag> beta-vX.Y.Z or vX.Y.Z; must not exist locally or on origin
7+# --dry-run do everything up to and including the summary, then stop: no tag, no push, no release.
8+# (a dirty tree / unpushed commits are reported but do not stop a dry run)
9+# --yes do not ask before tagging and publishing
10+#
11+# Needs: git, gh (authenticated), make, gcc, cargo (mnemo), GNU tar, sha256sum, curl, bwrap (smoke test).
12+# See docs/RELEASING.md. POSIX sh.
13+set -eu
14+
15+ROOT=$(cd "$(dirname "$0")/.." && pwd)
16+cd "$ROOT"
17+
18+# Platforms this script can build, test AND publish. It builds for the machine it runs on.
19+SUPPORTED="linux-x86_64"
20+
21+die() { printf 'release: %s\n' "$*" >&2; exit 1; }
22+say() { printf '\n== %s\n' "$*"; }
23+DRY=0; YES=0; TAG=""
24+for a in "$@"; do
25+ case $a in
26+ --dry-run) DRY=1 ;;
27+ --yes) YES=1 ;;
28+ -h|--help) sed -n '2,13p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
29+ -*) die "unknown option $a" ;;
30+ *) [ -z "$TAG" ] || die "only one tag allowed"; TAG=$a ;;
31+ esac
32+done
33+[ -n "$TAG" ] || die "usage: tools/release.sh <tag> [--dry-run] [--yes]"
34+
35+# 1. tag format, tag must be new ------------------------------------------------------------------
36+say "tag $TAG"
37+printf '%s\n' "$TAG" | grep -Eq '^(beta-)?v[0-9]+\.[0-9]+\.[0-9]+$' \
38+ || die "tag must look like beta-v0.1.0 or v0.1.0"
39+git rev-parse -q --verify "refs/tags/$TAG" >/dev/null && die "tag $TAG already exists locally; releases never reuse a tag"
40+git remote get-url origin >/dev/null 2>&1 || die "no origin remote"
41+git fetch -q origin --tags
42+git rev-parse -q --verify "refs/tags/$TAG" >/dev/null && die "tag $TAG already exists on origin; releases never reuse a tag"
43+REPO=$(git remote get-url origin | sed -E 's#^(git@github.com:|https://github.com/)##; s#\.git$##')
44+printf 'repo %s\n' "$REPO"
45+
46+# 2. git state and gh ------------------------------------------------------------------------------
47+say "git state"
48+problems=""
49+[ "$(git rev-parse --abbrev-ref HEAD)" = main ] || problems="$problems\n - not on main"
50+[ -z "$(git status --porcelain)" ] || problems="$problems\n - working tree is not clean"
51+[ "$(git rev-parse HEAD)" = "$(git rev-parse origin/main)" ] || problems="$problems\n - HEAD is not origin/main (push or pull first)"
52+if [ -n "$problems" ]; then
53+ if [ "$DRY" = 1 ]; then printf 'dry run: a real release would stop here:%b\n' "$problems"
54+ else die "refusing to release:$(printf '%b' "$problems")"; fi
55+else
56+ echo "on main, clean, equal to origin/main"
57+fi
58+command -v gh >/dev/null || die "gh is not installed"
59+gh auth status >/dev/null 2>&1 || die "gh is not authenticated (gh auth login)"
60+
61+# platform of this machine
62+OS=$(uname -s | tr 'A-Z' 'a-z')
63+case $(uname -m) in x86_64|amd64) ARCH=x86_64 ;; aarch64|arm64) ARCH=aarch64 ;; *) ARCH=$(uname -m) ;; esac
64+PLAT=$OS-$ARCH
65+case " $SUPPORTED " in *" $PLAT "*) ;; *) die "$PLAT is not a supported release platform (supported: $SUPPORTED); see docs/RELEASING.md" ;; esac
66+command -v bwrap >/dev/null || die "bwrap is needed for the smoke test sandbox"
67+
68+# 8 (early, it is cheap). release notes from CHANGELOG.md ------------------------------------------
69+say "release notes"
70+WORK=$(mktemp -d); trap 'rm -rf "$WORK"' EXIT
71+NOTES=$WORK/notes.md
72+awk -v tag="$TAG" '
73+ /^## / { if (on) exit; h = $0; i = index(h, tag); if (i) { c = substr(h, i + length(tag), 1); p = i > 1 ? substr(h, i - 1, 1) : " "; if (c !~ /[0-9.]/ && p !~ /[A-Za-z0-9-]/) on = 1 } next }
74+ on { print }
75+' CHANGELOG.md > "$NOTES"
76+[ -s "$NOTES" ] && grep -q '[^[:space:]]' "$NOTES" \
77+ || die "CHANGELOG.md has no '## <date> ($TAG ...)' section for $TAG; write it first"
78+echo "$(wc -l < "$NOTES") lines of notes from CHANGELOG.md"
79+
80+# 3. clean build with the version injected ---------------------------------------------------------
81+# Exported so every nested make (including the ones the test scripts start) links the same way.
82+say "clean build"
83+ASH_VERSION=$TAG; ASH_STATIC_RT=1; export ASH_VERSION ASH_STATIC_RT
84+make clean
85+make
86+
87+# 4. tests ------------------------------------------------------------------------------------------
88+say "tests"
89+for t in hearth/mnemo/tests/scripts/run_regression.sh hearth/front/tests/scripts/check_types.sh \
90+ hearth/ashvm/tests/scripts/run_regression.sh hearth/kiln/tests/scripts/run_regression.sh \
91+ hearth/apm/tests/scripts/smoke_test.sh; do
92+ printf '%s ... ' "$t"
93+ if "$t" > "$WORK/test.log" 2>&1; then
94+ echo ok
95+ else
96+ tail -30 "$WORK/test.log"; die "test failed: $t"
97+ fi
98+done
99+
100+# 5. package ------------------------------------------------------------------------------------------
101+say "package"
102+rm -rf dist; mkdir dist
103+STAGE=$WORK/stage; mkdir -p "$STAGE/bin"
104+for b in ashvm kiln apm; do install -m 755 "bin/$b" "$STAGE/bin/$b"; strip "$STAGE/bin/$b"; done
105+printf '%s\n' "$TAG" > "$STAGE/VERSION"
106+ASSET=ash-$PLAT.tar.gz
107+tar --sort=name --owner=0 --group=0 --numeric-owner --mtime=@0 -czf "dist/$ASSET" -C "$STAGE" bin VERSION
108+( cd dist && sha256sum ash-*.tar.gz > SHA256SUMS )
109+tar -tzf "dist/$ASSET"
110+
111+# 6. smoke test: a sandbox with only the libraries the binaries list, no PATH, no home, no network ---
112+say "smoke test ($ASSET)"
113+HOME_ASH=$WORK/ash; mkdir -p "$HOME_ASH"; tar -xzf "dist/$ASSET" -C "$HOME_ASH"
114+[ "$(cat "$HOME_ASH/VERSION")" = "$TAG" ] || die "VERSION file does not say $TAG"
115+mkdir -p "$WORK/cwd"; cp examples/HelloWorld.ash "$WORK/cwd/"
116+LIBS=$(for b in ashvm kiln apm; do ldd "$HOME_ASH/bin/$b"; done | awk '/=>/ {print $3} !/=>/ && /^[ \t]*\// {print $1}' | sort -u)
117+echo "runtime libraries used by the binaries:"; for l in $LIBS; do echo " $l"; done
118+BW="bwrap --unshare-all --die-with-parent --clearenv --ro-bind $HOME_ASH /ash --bind $WORK/cwd /work --chdir /work --proc /proc --dev /dev"
119+for l in $LIBS; do BW="$BW --ro-bind $(readlink -f "$l") $l"; done
120+INTERP=$(readelf -l "$HOME_ASH/bin/kiln" | sed -n 's/.*interpreter: \(.*\)\]/\1/p')
121+BW="$BW --ro-bind $(readlink -f "$INTERP") $INTERP"
122+sandbox() { $BW "$@"; }
123+for b in ashvm kiln apm; do
124+ got=$(sandbox /ash/bin/$b --version) || die "$b --version failed in the sandbox"
125+ [ "$got" = "$b $TAG" ] || die "$b --version printed '$got', expected '$b $TAG'"
126+ echo "$got"
127+done
128+got=$(sandbox /ash/bin/ashvm run HelloWorld.ash) || die "ashvm run failed in the sandbox"
129+[ "$got" = "Hello, Ash!" ] || die "ashvm run printed '$got'"
130+echo "ashvm run HelloWorld.ash -> $got"
131+sandbox /ash/bin/kiln HelloWorld.ash -o /work/hello || die "kiln failed in the sandbox"
132+got=$(sandbox /work/hello) || die "the kiln-compiled program failed"
133+[ "$got" = "Hello, Ash!" ] || die "kiln output printed '$got'"
134+echo "kiln HelloWorld.ash -> ./hello -> $got"
135+sandbox /ash/bin/apm new smoke >/dev/null || die "apm new failed in the sandbox"
136+[ -f "$WORK/cwd/smoke/ash.pkg" ] || die "apm new created no ash.pkg"
137+echo "apm new smoke -> ash.pkg created"
138+
139+# 7. checksums ------------------------------------------------------------------------------------------
140+say "checksums"
141+( cd dist && sha256sum -c SHA256SUMS )
142+
143+# 9. summary ---------------------------------------------------------------------------------------------
144+say "summary"
145+echo "tag $TAG (title: Ash $TAG, latest, not a pre-release)"
146+echo "commit $(git rev-parse --short HEAD)"
147+echo "assets:"
148+for f in dist/*; do printf ' %-26s %8s bytes\n' "$(basename "$f")" "$(wc -c < "$f")"; done
149+echo "SHA256SUMS:"; sed 's/^/ /' dist/SHA256SUMS
150+echo "download https://github.com/$REPO/releases/latest/download/<asset>"
151+echo "notes ----"; cat "$NOTES"; echo "----"
152+if [ "$DRY" = 1 ]; then echo; echo "dry run: nothing was tagged, pushed or published."; exit 0; fi
153+if [ "$YES" != 1 ]; then
154+ printf '\nTag %s, push it and publish the release on %s? [y/N] ' "$TAG" "$REPO"
155+ read -r ans
156+ case $ans in y|Y|yes) ;; *) echo "aborted; dist/ is kept."; exit 1 ;; esac
157+fi
158+
159+# 10. tag and publish ---------------------------------------------------------------------------------------
160+say "publish"
161+git tag -a "$TAG" -m "Ash $TAG"
162+git push origin "refs/tags/$TAG"
163+gh release create "$TAG" dist/* --repo "$REPO" --title "Ash $TAG" --notes-file "$NOTES" --latest --verify-tag \
164+ || die "the tag $TAG is pushed but the release was not created. Do not reuse the tag: fix it per docs/RELEASING.md"
165+
166+# 11. verify what the public URL serves ------------------------------------------------------------------------
167+say "verify download"
168+V=$WORK/verify; mkdir -p "$V"
169+base=https://github.com/$REPO/releases/latest/download
170+curl -fsSL "$base/SHA256SUMS" -o "$V/SHA256SUMS"
171+curl -fsSL "$base/$ASSET" -o "$V/$ASSET"
172+( cd "$V" && grep " $ASSET\$" SHA256SUMS | sha256sum -c - ) || die "the published $ASSET does not match the published SHA256SUMS"
173+cmp -s "$V/SHA256SUMS" dist/SHA256SUMS || die "the published SHA256SUMS differs from the local one"
174+echo "released $TAG: $base/$ASSET"