# 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/`, 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 && /^\[[^]]+\]: / {next} 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 build for Apple silicon Macs (M1 and later). 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 binary 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