Files
MeshTerm/.github/workflows/github-release.yml
T
Jean-Pierre MartineauandClaude Opus 5 59eccba0a0 Every build runs on 3.14
JP builds with 3.14 now, so the workflows do too. The lint job moves from 3.10, the
packaging build job and the release's site rebuild and PyPI build from 3.12, and the
installers from 3.10. The test matrix is untouched: it still runs every version from
3.10 up, because 3.10 is still the floor.

The installers' comment said the oldest Python was chosen on purpose, for reach. The
reach that matters on Linux comes from glibc, and that comes from the runner, so the
Linux legs stay on ubuntu-22.04 and the comment now says that. The binaries carry 3.14
from the next release; 0.9.0's were built on 3.10.

The changelog gains the 3.14 entry JP wrote in another session, plus a line for the
downloads.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-21 21:14:52 -04:00

267 lines
11 KiB
YAML

# Puts the downloadable builds on the repository's Releases page.
#
# Nothing here leaves GitHub: a release is exactly as visible as the repository it sits
# on. This is *not* the PyPI publish — that is `release.yml`, it is a separate act, and it
# does not run from here.
#
# Triggered by pushing a version tag, which is the moment a release exists in git anyway:
#
# git tag -a v0.3.0 -m "MeshTerm 0.3.0"
# git push origin v0.3.0
#
# The binaries are built by `installers.yml`, called rather than copied, so the files
# people download are made exactly the way the ones CI checks on every push are.
#
# Once the release exists, `website` puts it on meshterm.net: it rebuilds that site from
# the tagged tree — the landing and About pages carry the version — and pushes the result
# to jpmartineau/meshterm.net, which GitHub Pages serves. It writes with a deploy key
# scoped to that one repository (secret SITE_DEPLOY_KEY); that repo's README makes one.
name: GitHub release
on:
push:
tags: ["v*"]
workflow_dispatch:
inputs:
tag:
description: "Existing tag to build a release from (e.g. v0.2.0)"
required: true
type: string
prerelease:
description: "Mark as a pre-release (keeps it out of 'Latest')"
type: boolean
default: false
permissions:
contents: read
jobs:
build:
uses: ./.github/workflows/installers.yml
publish:
name: create the release
needs: build
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
# The one job that needs it: creating a release writes to the repository.
contents: write
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.tag || github.ref }}
# Every platform's artifact, flattened into one directory of files to attach.
- uses: actions/download-artifact@v4
with:
# The build uploads one artifact nobody should ever download: the unsigned
# Windows binary it hands to SignPath. Everything meant for the release page is
# named `meshterm-…`, so the pattern is the filter.
pattern: meshterm-*
path: artifacts
merge-multiple: true
- name: Gather the binaries and their checksums
id: gather
shell: bash
run: |
tag="${{ inputs.tag || github.ref_name }}"
version="${tag#v}"
echo "tag=$tag" >> "$GITHUB_OUTPUT"
echo "version=$version" >> "$GITHUB_OUTPUT"
mkdir -p release
find artifacts -type f -exec mv {} release/ \;
ls -lh release/
cd release
# A second, version-less copy of every binary. GitHub's evergreen download URL,
# `releases/latest/download/<name>`, resolves against an exact asset name and
# nothing else — so a link that always fetches the newest build needs a name
# that never moves. That is what lets the README, the website and anyone's
# install script say `meshterm-macos-arm64` once instead of naming a version
# that goes stale the moment the next release lands. The version-stamped file
# stays beside it: it is the one you want on disk when you keep several builds
# around, and it is what the release-notes install block points at, since that
# page is about this release rather than the latest one.
made=0
for versioned in "meshterm-$version"-*; do
case "$versioned" in *.sha256) continue ;; esac
[ -e "$versioned" ] || continue
stable="meshterm-${versioned#meshterm-$version-}"
cp "$versioned" "$stable"
sha256sum "$stable" > "$stable.sha256"
made=$((made + 1))
done
# Nothing matched means the tag and `meshterm.__version__` disagree, which is a
# broken release either way: the notes below are about to name files by version
# that the build did not produce. Fail here, where it is one line to read.
if [ "$made" -eq 0 ]; then
echo "::error::No binary named meshterm-$version-*; $tag and meshterm.__version__ disagree."
exit 1
fi
echo "$made version-less aliases made"
# One combined checksum file is easier to verify against than ten loose ones.
cat *.sha256 > SHA256SUMS && rm -f *.sha256
- name: Take the release notes from the changelog
id: notes
shell: bash
env:
VERSION: ${{ steps.gather.outputs.version }}
run: |
# The section for this version, between its own heading and the next one. If the
# changelog has nothing for it, say so plainly rather than inventing notes.
#
# Extracted once, into the file, and the *file* is what gets tested. It used to
# run twice — once down a `| sed | head -1 | grep -q .` pipeline to decide, then
# again for real — and this step runs under `bash -e -o pipefail`, where that
# pipeline is a race it can lose. `head -1` exits the moment it has its line and
# closes the pipe; if `sed` still has bytes to write it dies on SIGPIPE, pipefail
# promotes that to a failed condition, and a version with a perfectly good
# changelog section publishes "No changelog entry" instead. It only bites once a
# section outgrows one buffered write, which is why every entry under 4 KB was
# fine and 0.3.1 was not — while 0.3.0, the same size, happened to win.
awk -v v="$VERSION" '
$0 ~ "^## \\[" v "\\]" {found=1; next}
found && /^## \[/ {exit}
found {print}
' CHANGELOG.md > notes.md
if ! grep -q '[^[:space:]]' notes.md; then
echo "No changelog entry for $VERSION." > notes.md
fi
echo "--- notes ---"; cat notes.md
# Most people who open a release page came to install the thing, so the commands go
# above the changelog rather than in a link to a README that names some other
# version. Written here rather than kept as prose anywhere means they can only ever
# name *this* release: the tag interpolates, so the URLs are the assets attached to
# the very page they are printed on, and there is no example version to go stale.
#
# The heredoc is quoted, so the markdown's own backticks stay backticks instead of
# becoming command substitution, and the two placeholders are filled by `sed`
# afterwards. Neither a tag nor a version can contain a slash, so the substitution
# needs no escaping.
- name: Open the notes with this release's install commands
shell: bash
env:
TAG: ${{ steps.gather.outputs.tag }}
VERSION: ${{ steps.gather.outputs.version }}
run: |
cat > install.md.in <<'INSTALL_MD'
## Install
One file, no Python needed.
**macOS** — paste these into Terminal. Use `curl`, **not your browser**: a browser
flags the download and macOS then refuses to open it at all, and on Sequoia and
later right-click → Open no longer gets you past it. `curl` flags nothing, so it
simply runs.
```bash
curl -fL -o meshterm https://github.com/jpmartineau/MeshTerm/releases/download/@TAG@/meshterm-@VERSION@-macos-arm64
chmod +x meshterm
./meshterm
```
That is the Apple silicon build — every Mac since 2020. On an Intel Mac, swap
`arm64` for `x64`.
**Linux** — the same three lines with `linux-x64`, or `linux-arm64` on a uConsole
or another 64-bit ARM handheld.
```bash
curl -fL -o meshterm https://github.com/jpmartineau/MeshTerm/releases/download/@TAG@/meshterm-@VERSION@-linux-x64
chmod +x meshterm
./meshterm
```
**Windows** — download [meshterm-@VERSION@-windows-x64.exe](https://github.com/jpmartineau/MeshTerm/releases/download/@TAG@/meshterm-@VERSION@-windows-x64.exe), then run it from Windows
Terminal or PowerShell rather than by double-clicking, so you can read anything it
says on the way up.
```powershell
.\meshterm-@VERSION@-windows-x64.exe
```
Every file 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
being told its number. [Installing with pip or pipx, and the rest of the
detail](https://github.com/jpmartineau/MeshTerm#install).
---
## What changed
INSTALL_MD
sed -e "s/@TAG@/$TAG/g" -e "s/@VERSION@/$VERSION/g" install.md.in > install.md
cat install.md notes.md > body.md
echo "--- body ---"; cat body.md
- name: Create it
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ steps.gather.outputs.tag }}
PRERELEASE: ${{ inputs.prerelease && '--prerelease' || '' }}
run: |
gh release create "$TAG" release/* \
--title "MeshTerm ${TAG#v}" \
--notes-file body.md \
--verify-tag \
$PRERELEASE
echo "https://github.com/${{ github.repository }}/releases/tag/$TAG"
website:
name: put the version on meshterm.net
needs: publish
# A pre-release stays out of "Latest", so it stays off the landing page too.
if: ${{ !inputs.prerelease }}
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Check for the deploy key
env:
SITE_DEPLOY_KEY: ${{ secrets.SITE_DEPLOY_KEY }}
run: |
if [ -z "$SITE_DEPLOY_KEY" ]; then
echo "::error::SITE_DEPLOY_KEY is not set; the meshterm.net README says how to make it."
exit 1
fi
- uses: actions/checkout@v4
with:
ref: ${{ inputs.tag || github.ref }}
path: meshterm
- uses: actions/checkout@v4
with:
repository: jpmartineau/meshterm.net
ssh-key: ${{ secrets.SITE_DEPLOY_KEY }}
path: site
- uses: actions/setup-python@v5
with:
python-version: "3.14"
- name: Rebuild the site from this release
run: |
python -m pip install --quiet -r site/requirements.txt
python site/build.py --meshterm meshterm
- name: Publish it
working-directory: site
env:
TAG: ${{ inputs.tag || github.ref_name }}
run: |
git add -A
if git diff --cached --quiet; then
echo "meshterm.net already shows ${TAG#v}."
exit 0
fi
git config user.name "github-actions[bot]"
git config user.email "41898341+github-actions[bot]@users.noreply.github.com"
git commit -m "MeshTerm ${TAG#v}"
git push