← 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
12 files changed+305−5
.gitignoremodified
+2−0@@ -1,6 +1,7 @@11
# Build artifacts22
*.o33
/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.md3435
.agents3536
skills-lock.json3637
docs/*38
+!docs/RELEASING.md3739
spec/3840
3941
# Private NotesCHANGELOG.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 License2
+3
+Copyright (c) 2026 lennoxrose4
+5
+Permission is hereby granted, free of charge, to any person obtaining a copy6
+of this software and associated documentation files (the "Software"), to deal7
+in the Software without restriction, including without limitation the rights8
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell9
+copies of the Software, and to permit persons to whom the Software is10
+furnished to do so, subject to the following conditions:11
+12
+The above copyright notice and this permission notice shall be included in all13
+copies or substantial portions of the Software.14
+15
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR16
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,17
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE18
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER19
+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 THE21
+SOFTWARE.Makefilemodified
+6−1@@ -7,7 +7,7 @@77
HEARTH := hearth88
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-%,%,$@) clean2626
27
+# `make release VERSION=beta-v0.1.0 [ARGS=--dry-run]`: see docs/RELEASING.md28
+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 Ash2
+3
+One command cuts a release:4
+5
+```sh6
+make release VERSION=beta-v0.2.0 # builds, tests, packages, asks, then publishes7
+make release VERSION=beta-v0.2.0 ARGS=--dry-run # everything except tag / push / publish8
+tools/release.sh beta-v0.2.0 --yes # same thing without `make`, no confirmation prompt9
+```10
+11
+Tags are `beta-vX.Y.Z` or `vX.Y.Z`. The release is published as the **latest** release and never as a12
+pre-release (GitHub's `/releases/latest/download/...` ignores pre-releases); "beta" lives in the tag, the13
+title (`Ash <tag>`) and the notes.14
+15
+## Prerequisites16
+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; the19
+ 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`) and23
+ network access (the `apm` smoke test clones from GitHub).24
+25
+## What the tool does26
+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 shared33
+ 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 is40
+needed at run time: `kiln` writes static executables and finds no files of its own.41
+42
+## Adding a platform43
+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 and47
+ `uname -m` normalised to `x86_64` / `aarch64`) to `SUPPORTED` in `tools/release.sh` once it passes48
+ 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 existing50
+ release with `gh release upload <tag> ash-<os>-<arch>.tar.gz`, then regenerate `SHA256SUMS` for all51
+ tarballs and upload it with `--clobber`. Do not ship a platform that was not built and run.52
+53
+## Fixing a bad release54
+55
+Never move or reuse a tag. Delete the release and the tag, then release the next version:56
+57
+```sh58
+gh release delete <tag> --cleanup-tag --yes # removes the release and the remote tag59
+git tag -d <tag> # the local tag60
+make release VERSION=<next tag> # with a CHANGELOG section for the next tag61
+```hearth/apm/Makefilemodified
+3−1@@ -1,6 +1,8 @@11
CC = gcc22
.DEFAULT_GOAL := all3
-CFLAGS = -Wall -Wextra -std=c11 -O2 -Isrc3
+# make ASH_VERSION=<tag> bakes the release tag into --version (tools/release.sh does this)4
+ASH_VERSION ?= dev5
+CFLAGS = -DASH_VERSION='"$(ASH_VERSION)"' -Wall -Wextra -std=c11 -O2 -Isrc46
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_VERSION7
+#define ASH_VERSION "dev"8
+#endif9
+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 ?= 02424
ifeq ($(GC),1)2525
GC_FLAGS = -DASH_GC2626
endif27
-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 ?= dev29
+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 binary31
+# needs only libc, libm, libgomp and the Vulkan loader on the target machine (libgomp has no static library32
+# on every distro). Same code, only the link differs.33
+ASH_STATIC_RT ?= 034
+ifeq ($(ASH_STATIC_RT),1)35
+STDCXX = -static-libgcc -Wl,-Bstatic -lstdc++ -Wl,-Bdynamic36
+else37
+STDCXX = -lstdc++38
+endif39
+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_VERSION12
+#define ASH_VERSION "dev"13
+#endif14
+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 = gcc22
.DEFAULT_GOAL := all33
KILN_RPATH_LIST = $(abspath ../../bin/extensions/pyre):/usr/local/lib/kiln4
-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 ?= dev6
+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 -lm68
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_VERSION18
+#define ASH_VERSION "dev"19
+#endif20
+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/.dll4146
// (Plan B) and exits, no .ash input at all -- an entirely separate4247
// mode from compiling a program.tools/release.shadded
+174−0@@ -0,0 +1,174 @@1
+#!/bin/sh2
+# 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 origin7
+# --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 publishing10
+#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 -eu14
+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 "$@"; do25
+ case $a in26
+ --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
+ esac32
+done33
+[ -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 --tags42
+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" ]; then53
+ 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")"; fi55
+else56
+ echo "on main, clean, equal to origin/main"57
+fi58
+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 machine62
+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) ;; esac64
+PLAT=$OS-$ARCH65
+case " $SUPPORTED " in *" $PLAT "*) ;; *) die "$PLAT is not a supported release platform (supported: $SUPPORTED); see docs/RELEASING.md" ;; esac66
+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"' EXIT71
+NOTES=$WORK/notes.md72
+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_RT84
+make clean85
+make86
+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; do92
+ printf '%s ... ' "$t"93
+ if "$t" > "$WORK/test.log" 2>&1; then94
+ echo ok95
+ else96
+ tail -30 "$WORK/test.log"; die "test failed: $t"97
+ fi98
+done99
+100
+# 5. package ------------------------------------------------------------------------------------------101
+say "package"102
+rm -rf dist; mkdir dist103
+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"; done105
+printf '%s\n' "$TAG" > "$STAGE/VERSION"106
+ASSET=ash-$PLAT.tar.gz107
+tar --sort=name --owner=0 --group=0 --numeric-owner --mtime=@0 -czf "dist/$ASSET" -C "$STAGE" bin VERSION108
+( 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"; done118
+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"; done120
+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; do124
+ 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
+done128
+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")"; done149
+echo "SHA256SUMS:"; sed 's/^/ /' dist/SHA256SUMS150
+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; fi153
+if [ "$YES" != 1 ]; then154
+ printf '\nTag %s, push it and publish the release on %s? [y/N] ' "$TAG" "$REPO"155
+ read -r ans156
+ case $ans in y|Y|yes) ;; *) echo "aborted; dist/ is kept."; exit 1 ;; esac157
+fi158
+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/download170
+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"