Merge pull request #211 from levineuwirth/distribution-stage1
Distribution Stage 1: binaries on tag
This commit is contained in:
commit
000b6cd333
|
|
@ -0,0 +1,362 @@
|
||||||
|
name: Release
|
||||||
|
|
||||||
|
# Distribution Stage 1 — binaries on tag. See
|
||||||
|
# docs/distribution-stage1-framing.md.
|
||||||
|
#
|
||||||
|
# Scope is deliberately one stage: build the two shipped binaries when a
|
||||||
|
# `v*` tag is pushed and attach them to a GitHub Release. There are no
|
||||||
|
# channels, no rollback, no update-in-place, no signing. Those are named
|
||||||
|
# as out of scope in the framing's §5 rather than forgotten.
|
||||||
|
#
|
||||||
|
# Nothing here runs on a branch push or a pull request. A tag is the
|
||||||
|
# only trigger, because a release built from anything else would publish
|
||||||
|
# artifacts for a commit nobody reviewed as a release.
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
|
||||||
|
# A release must never race itself. Two tags pushed close together would
|
||||||
|
# otherwise both mutate the same release page.
|
||||||
|
concurrency:
|
||||||
|
group: release-${{ github.ref }}
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
env:
|
||||||
|
CARGO_TERM_COLOR: always
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
# ---------------------------------------------------------------
|
||||||
|
# Preflight runs BEFORE any build, because both of its checks catch
|
||||||
|
# mistakes that are cheap now and expensive once artifacts exist: a
|
||||||
|
# published release is a public URL, and unpublishing one is not the
|
||||||
|
# same as never having published it.
|
||||||
|
# ---------------------------------------------------------------
|
||||||
|
preflight:
|
||||||
|
name: Preflight (tag/version, ancestry)
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
timeout-minutes: 10
|
||||||
|
outputs:
|
||||||
|
version: ${{ steps.check.outputs.version }}
|
||||||
|
prerelease: ${{ steps.check.outputs.prerelease }}
|
||||||
|
steps:
|
||||||
|
# Full history: the ancestry check below cannot run on a shallow
|
||||||
|
# clone, and the default checkout depth is 1.
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- id: check
|
||||||
|
name: Tag must match the root crate version, and be on main
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
tag="${GITHUB_REF_NAME}"
|
||||||
|
|
||||||
|
# Strip the leading v, then strip any prerelease suffix:
|
||||||
|
# v1.1.0-rc.1 and v1.1.0 must both match a crate version of
|
||||||
|
# 1.1.0. Cargo has no place to record "rc.1", so requiring the
|
||||||
|
# manifest to carry it would make prereleases impossible.
|
||||||
|
version="${tag#v}"
|
||||||
|
base="${version%%-*}"
|
||||||
|
|
||||||
|
# Read the ROOT package version specifically. A grep for
|
||||||
|
# `^version` across the workspace would match whichever
|
||||||
|
# manifest sorted first, and pmacs-protocol deliberately holds
|
||||||
|
# a different number.
|
||||||
|
manifest=$(cargo metadata --no-deps --format-version 1 \
|
||||||
|
| python3 -c 'import json,sys; print(next(p["version"] for p in json.load(sys.stdin)["packages"] if p["name"]=="pmacs"))')
|
||||||
|
|
||||||
|
echo "tag=$tag base=$base manifest=$manifest"
|
||||||
|
if [ "$base" != "$manifest" ]; then
|
||||||
|
echo "::error::tag $tag implies version $base but the pmacs crate is $manifest." \
|
||||||
|
"Bump Cargo.toml or fix the tag; publishing this would ship a binary whose" \
|
||||||
|
"--version disagrees with its release."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# A tag on a branch is the realistic mistake. Re-running the
|
||||||
|
# test suite here would be duplicated cost -- CI already tested
|
||||||
|
# this commit -- but nothing otherwise enforces that the tagged
|
||||||
|
# commit is one CI ever saw on main.
|
||||||
|
if ! git merge-base --is-ancestor "$GITHUB_SHA" origin/main; then
|
||||||
|
echo "::error::tagged commit $GITHUB_SHA is not an ancestor of origin/main." \
|
||||||
|
"Releases are cut from main."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Anything with a suffix (-rc.1, -beta) is a prerelease. The
|
||||||
|
# framing requires the RC to be marked so it cannot be mistaken
|
||||||
|
# for the real thing on the releases page.
|
||||||
|
if [ "$version" = "$base" ]; then
|
||||||
|
echo "prerelease=false" >> "$GITHUB_OUTPUT"
|
||||||
|
else
|
||||||
|
echo "prerelease=true" >> "$GITHUB_OUTPUT"
|
||||||
|
fi
|
||||||
|
echo "version=$version" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------
|
||||||
|
# Both runners are PINNED, never `-latest`. Two different reasons,
|
||||||
|
# both about a silent choice:
|
||||||
|
#
|
||||||
|
# * ubuntu-22.04 sets the glibc floor at 2.35, which covers Ubuntu
|
||||||
|
# 22.04 and Debian 12. `ubuntu-latest` (24.04, glibc 2.39) would
|
||||||
|
# silently produce binaries that fail to load on both with a bare
|
||||||
|
# `GLIBC_2.39 not found`. Note the floor does NOT reach RHEL 9
|
||||||
|
# (glibc 2.34) -- that needs a container or cross-build and is
|
||||||
|
# parked in the framing's §5.
|
||||||
|
#
|
||||||
|
# * macos-15 is arm64. `macos-latest` drifts, so a future runner
|
||||||
|
# change could move the minimum supported macOS with no commit in
|
||||||
|
# this repository to point at.
|
||||||
|
# ---------------------------------------------------------------
|
||||||
|
build:
|
||||||
|
name: Build ${{ matrix.target-label }}
|
||||||
|
needs: preflight
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
timeout-minutes: 45
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- os: ubuntu-22.04
|
||||||
|
target-label: linux-x86_64
|
||||||
|
- os: macos-15
|
||||||
|
target-label: macos-arm64
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
|
- uses: Swatinem/rust-cache@v2
|
||||||
|
|
||||||
|
# CRDT is not optional. `run_gpu` refuses outright without it
|
||||||
|
# ("--gpu requires pmacs built with --features crdt"), and
|
||||||
|
# InstanceCapabilities::default advertises multi_frontend /
|
||||||
|
# crdt_replica / semantic_render only under the feature. A
|
||||||
|
# non-CRDT release would ship an editor that cannot use the GPU
|
||||||
|
# frontend shipped beside it.
|
||||||
|
#
|
||||||
|
# EXPLICIT --bin TARGETS, layer 1 of 2. Cargo auto-discovers
|
||||||
|
# src/bin/*.rs, so a plain `--workspace` release build also
|
||||||
|
# produces pmacs-audit, pmacs_fake_lsp and pmacs_fake_mcp -- the
|
||||||
|
# last two being test fixtures. Naming targets keeps them from
|
||||||
|
# being built at all.
|
||||||
|
- name: Build the shipped binaries only
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
cargo build --release --bin pmacs --features crdt
|
||||||
|
cargo build --release -p pmacs-gpu
|
||||||
|
|
||||||
|
# EXPLICIT STAGED ASSET LIST, layer 2 of 2. Layer 1 alone is not
|
||||||
|
# enough: a cached target/release can still hold binaries from an
|
||||||
|
# earlier build, and archiving that directory would publish them.
|
||||||
|
# Copying named files into a clean staging dir makes the archive's
|
||||||
|
# contents a decision rather than a directory's residue.
|
||||||
|
#
|
||||||
|
# The two binaries are staged into the SAME directory on purpose.
|
||||||
|
# `pmacs --gpu` prefers a co-located pmacs-gpu and only then falls
|
||||||
|
# back to a PATH lookup, so co-location is what makes an unpacked
|
||||||
|
# release self-contained for someone who unpacks it off PATH.
|
||||||
|
- name: Stage the archive
|
||||||
|
id: stage
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
name="pmacs-${{ needs.preflight.outputs.version }}-${{ matrix.target-label }}"
|
||||||
|
mkdir -p "staging/$name"
|
||||||
|
cp target/release/pmacs "staging/$name/"
|
||||||
|
cp target/release/pmacs-gpu "staging/$name/"
|
||||||
|
cp README.md LICENSE* "staging/$name/" 2>/dev/null || true
|
||||||
|
chmod +x "staging/$name/pmacs" "staging/$name/pmacs-gpu"
|
||||||
|
tar -C staging -czf "$name.tar.gz" "$name"
|
||||||
|
echo "archive=$name.tar.gz" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "name=$name" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
# Assert the archive rather than the build command. The build
|
||||||
|
# could change, a target could be added, a cache could leak -- the
|
||||||
|
# only thing that decides what users receive is what is inside
|
||||||
|
# this file.
|
||||||
|
- name: Assert archive contents
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
archive="${{ steps.stage.outputs.archive }}"
|
||||||
|
name="${{ steps.stage.outputs.name }}"
|
||||||
|
|
||||||
|
members=$(tar -tzf "$archive" | sed "s#^$name/##" | grep -v '^$' | sort)
|
||||||
|
echo "members:"; echo "$members" | sed 's/^/ /'
|
||||||
|
|
||||||
|
for forbidden in pmacs-audit pmacs_fake_lsp pmacs_fake_mcp; do
|
||||||
|
if echo "$members" | grep -qx "$forbidden"; then
|
||||||
|
echo "::error::$forbidden is present in $archive and must never ship"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
for required in pmacs pmacs-gpu; do
|
||||||
|
echo "$members" | grep -qx "$required" \
|
||||||
|
|| { echo "::error::$required missing from $archive"; exit 1; }
|
||||||
|
done
|
||||||
|
|
||||||
|
# Executable bits, read back out of the archive itself.
|
||||||
|
for required in pmacs pmacs-gpu; do
|
||||||
|
mode=$(tar -tvzf "$archive" | awk -v f="$name/$required" '$NF==f {print $1}')
|
||||||
|
case "$mode" in
|
||||||
|
*x*) : ;;
|
||||||
|
*) echo "::error::$required is not executable in the archive (mode $mode)"; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# Run the staged binaries. `--version` is the cheapest end-to-end
|
||||||
|
# proof that what was built is what will ship, and it is the check
|
||||||
|
# that would have caught a tag/manifest mismatch reaching this far.
|
||||||
|
- name: Assert the staged binaries report the tagged version
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
expected="${{ needs.preflight.outputs.version }}"
|
||||||
|
base="${expected%%-*}"
|
||||||
|
name="${{ steps.stage.outputs.name }}"
|
||||||
|
|
||||||
|
got_pmacs=$("staging/$name/pmacs" --version)
|
||||||
|
got_gpu=$("staging/$name/pmacs-gpu" --version)
|
||||||
|
echo "pmacs: $got_pmacs"
|
||||||
|
echo "pmacs-gpu: $got_gpu"
|
||||||
|
|
||||||
|
echo "$got_pmacs" | grep -qx "pmacs $base" \
|
||||||
|
|| { echo "::error::pmacs --version reported '$got_pmacs', expected 'pmacs $base'"; exit 1; }
|
||||||
|
echo "$got_gpu" | grep -q "^pmacs-gpu $base " \
|
||||||
|
|| { echo "::error::pmacs-gpu --version reported '$got_gpu', expected 'pmacs-gpu $base ...'"; exit 1; }
|
||||||
|
|
||||||
|
# The glibc floor, MACHINE-CHECKED rather than trusted.
|
||||||
|
#
|
||||||
|
# Pinning ubuntu-22.04 is what sets the floor, but nothing about a
|
||||||
|
# pinned runner *proves* the resulting binary honours it, and the
|
||||||
|
# failure is invisible at build time -- it surfaces as a bare
|
||||||
|
# `GLIBC_2.xx not found` on a user's machine, with no clue which
|
||||||
|
# commit caused it. Reading the required symbol versions out of
|
||||||
|
# the binary turns a runner choice into an assertion, so switching
|
||||||
|
# this job to `ubuntu-latest` fails HERE instead of shipping.
|
||||||
|
#
|
||||||
|
# Linux only: Mach-O has no equivalent versioned-symbol scheme.
|
||||||
|
- name: Assert the glibc floor
|
||||||
|
if: runner.os == 'Linux'
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
floor="2.35" # Ubuntu 22.04 / Debian 12; see the framing §1.6
|
||||||
|
name="${{ steps.stage.outputs.name }}"
|
||||||
|
fail=0
|
||||||
|
for bin in pmacs pmacs-gpu; do
|
||||||
|
max=$(objdump -T "staging/$name/$bin" \
|
||||||
|
| grep -oE 'GLIBC_[0-9]+\.[0-9]+' \
|
||||||
|
| sed 's/GLIBC_//' | sort -uV | tail -1)
|
||||||
|
echo "$bin requires at most glibc $max (floor $floor)"
|
||||||
|
highest=$(printf '%s\n%s\n' "$floor" "$max" | sort -V | tail -1)
|
||||||
|
if [ "$highest" != "$floor" ]; then
|
||||||
|
echo "::error::$bin requires glibc $max, above the declared floor $floor." \
|
||||||
|
"It will not load on Ubuntu 22.04 or Debian 12. Check the runner image."
|
||||||
|
fail=1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
[ "$fail" -eq 0 ]
|
||||||
|
|
||||||
|
- name: Checksum
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
archive="${{ steps.stage.outputs.archive }}"
|
||||||
|
if command -v sha256sum >/dev/null; then
|
||||||
|
sha256sum "$archive" > "$archive.sha256"
|
||||||
|
else
|
||||||
|
shasum -a 256 "$archive" > "$archive.sha256"
|
||||||
|
fi
|
||||||
|
cat "$archive.sha256"
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ${{ steps.stage.outputs.name }}
|
||||||
|
path: |
|
||||||
|
${{ steps.stage.outputs.archive }}
|
||||||
|
${{ steps.stage.outputs.archive }}.sha256
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
publish:
|
||||||
|
name: Publish release
|
||||||
|
needs: [preflight, build]
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
timeout-minutes: 15
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
path: dist
|
||||||
|
merge-multiple: true
|
||||||
|
|
||||||
|
# One SHA256SUMS covering every published artifact, alongside the
|
||||||
|
# per-archive files. Without it a download cannot be verified by
|
||||||
|
# anyone; with it, verification is one command.
|
||||||
|
- name: Collect checksums
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
cd dist
|
||||||
|
ls -la
|
||||||
|
cat ./*.sha256 | sed 's#\./##' | sort -k2 > SHA256SUMS
|
||||||
|
echo "--- SHA256SUMS ---"; cat SHA256SUMS
|
||||||
|
sha256sum -c SHA256SUMS
|
||||||
|
|
||||||
|
- name: Release notes
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
cat > notes.md <<'NOTES'
|
||||||
|
## Install
|
||||||
|
|
||||||
|
Download the archive for your platform, unpack it, and put both
|
||||||
|
binaries somewhere on your `PATH` — **keep them together**:
|
||||||
|
`pmacs --gpu` looks for `pmacs-gpu` beside itself first, then
|
||||||
|
falls back to `PATH`.
|
||||||
|
|
||||||
|
Verify a download against `SHA256SUMS`:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sha256sum -c SHA256SUMS --ignore-missing
|
||||||
|
```
|
||||||
|
|
||||||
|
## Platform support
|
||||||
|
|
||||||
|
- **Linux x86_64** — built on Ubuntu 22.04, so the floor is
|
||||||
|
**glibc ≥ 2.35**. Covers Ubuntu 22.04+ and Debian 12+.
|
||||||
|
**RHEL 9 (glibc 2.34) is below the floor and not supported
|
||||||
|
yet.**
|
||||||
|
- **macOS arm64 (Apple Silicon)** — Intel macOS is not built
|
||||||
|
yet. The binaries are **unsigned and not notarized**, so
|
||||||
|
Gatekeeper will quarantine them; you will need to allow them
|
||||||
|
explicitly.
|
||||||
|
|
||||||
|
## Runtime dependencies
|
||||||
|
|
||||||
|
Not checked at startup — the editor assumes them:
|
||||||
|
`/bin/sh`, `stty` and coreutils (PTY raw-mode trampoline, used
|
||||||
|
by the REPL and terminal), plus `git` and `tar` for package
|
||||||
|
installation. The Lua VM is statically vendored, so there is no
|
||||||
|
external Lua dependency.
|
||||||
|
|
||||||
|
`pmacs-gpu` additionally needs a working Vulkan (Linux) or
|
||||||
|
Metal (macOS) adapter. There is no software-rasterizer
|
||||||
|
fallback in a shipped binary.
|
||||||
|
|
||||||
|
## Not in this release
|
||||||
|
|
||||||
|
Channels, in-place update, rollback, signing, reproducible
|
||||||
|
builds, package-manager distribution, and Windows. See
|
||||||
|
`docs/distribution-stage1-framing.md` §5.
|
||||||
|
NOTES
|
||||||
|
echo "notes written"
|
||||||
|
|
||||||
|
- name: Publish
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
flags=(--title "pmacs ${{ needs.preflight.outputs.version }}" --notes-file notes.md)
|
||||||
|
if [ "${{ needs.preflight.outputs.prerelease }}" = "true" ]; then
|
||||||
|
flags+=(--prerelease)
|
||||||
|
fi
|
||||||
|
gh release create "${GITHUB_REF_NAME}" "${flags[@]}" \
|
||||||
|
dist/*.tar.gz dist/SHA256SUMS
|
||||||
|
|
@ -2568,7 +2568,7 @@ checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pmacs"
|
name = "pmacs"
|
||||||
version = "1.0.0"
|
version = "1.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arborium-lean",
|
"arborium-lean",
|
||||||
"codebook-tree-sitter-latex",
|
"codebook-tree-sitter-latex",
|
||||||
|
|
@ -2621,7 +2621,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pmacs-gpu"
|
name = "pmacs-gpu"
|
||||||
version = "0.0.1"
|
version = "1.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arboard",
|
"arboard",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ unicode-width = "0.2"
|
||||||
[package]
|
[package]
|
||||||
name = "pmacs"
|
name = "pmacs"
|
||||||
default-run = "pmacs"
|
default-run = "pmacs"
|
||||||
version = "1.0.0"
|
version = "1.1.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
rust-version = "1.95"
|
rust-version = "1.95"
|
||||||
description = "Parallel Emacs --- a Rust-cored, Lua-scripted editor in the Emacs tradition"
|
description = "Parallel Emacs --- a Rust-cored, Lua-scripted editor in the Emacs tradition"
|
||||||
|
|
|
||||||
27
README.md
27
README.md
|
|
@ -141,6 +141,33 @@ loaded after the builtin runtime so plain assignments override
|
||||||
defaults --- keybindings, `pmacs.lsp.config`, theme overrides, and
|
defaults --- keybindings, `pmacs.lsp.config`, theme overrides, and
|
||||||
package installs all live there.
|
package installs all live there.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
Download an archive from the
|
||||||
|
[releases page](https://github.com/levineuwirth/pmacs/releases), unpack
|
||||||
|
it, and put both binaries somewhere on your `PATH`.
|
||||||
|
|
||||||
|
**Keep `pmacs` and `pmacs-gpu` together.** `pmacs --gpu` looks for
|
||||||
|
`pmacs-gpu` beside itself first and only then falls back to a `PATH`
|
||||||
|
lookup, so an unpacked release is self-contained as long as the two
|
||||||
|
stay in the same directory.
|
||||||
|
|
||||||
|
Verify a download:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sha256sum -c SHA256SUMS --ignore-missing
|
||||||
|
```
|
||||||
|
|
||||||
|
| platform | built on | notes |
|
||||||
|
|---|---|---|
|
||||||
|
| Linux x86_64 | Ubuntu 22.04 | requires **glibc ≥ 2.35** — Ubuntu 22.04+, Debian 12+. **RHEL 9 (glibc 2.34) is not supported yet.** |
|
||||||
|
| macOS arm64 | macOS 15 | Apple Silicon only; Intel is not built yet. Binaries are **unsigned and not notarized**, so Gatekeeper will quarantine them until you allow them explicitly. |
|
||||||
|
|
||||||
|
Releases carry binaries only — there is no in-place update, rollback, or
|
||||||
|
package-manager distribution yet. Build from source for any platform not
|
||||||
|
listed, and see "Runtime dependencies" below for what the editor assumes
|
||||||
|
is present.
|
||||||
|
|
||||||
## Build
|
## Build
|
||||||
|
|
||||||
Builds on the toolchain pinned in `rust-toolchain.toml` (Rust
|
Builds on the toolchain pinned in `rust-toolchain.toml` (Rust
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,436 @@
|
||||||
|
# Framing — Distribution Stage 1: binaries on tag
|
||||||
|
|
||||||
|
**Revision 3.** Status: **implemented** on branch `distribution-stage1`,
|
||||||
|
based on `githubsucks/main` @ `4984169` (#210). Approved at revision 2.
|
||||||
|
|
||||||
|
**Revision 2 → 3** records two implementation findings, not a new design
|
||||||
|
round:
|
||||||
|
|
||||||
|
- **Layer 2 of the binary exclusion is load-bearing, and this is now
|
||||||
|
demonstrated rather than argued** (§1.2b). The argument for it was
|
||||||
|
hypothetical; building the branch produced the exact case it guards
|
||||||
|
against, on the first try.
|
||||||
|
- **The glibc floor is machine-checked** (§1.6a), which is stronger than
|
||||||
|
acceptance 7's original container test and runs on every release
|
||||||
|
instead of once at RC time.
|
||||||
|
- **The version bump exposed a real product defect** (§1.3a): the daemon
|
||||||
|
reported the *protocol* crate's version to every attached frontend
|
||||||
|
under a field named `pmacs_version`. It was invisible while the two
|
||||||
|
crates happened to share a number, and Q#D1's decision to diverge them
|
||||||
|
is what surfaced it. Fixed here, because shipping a release whose
|
||||||
|
daemon misreports its own version is precisely what this stage
|
||||||
|
exists to prevent.
|
||||||
|
|
||||||
|
`.github/workflows/` contains exactly one workflow and it is test-only.
|
||||||
|
**There is no release job, no artifact upload, no tags-to-binaries path.**
|
||||||
|
Installing pmacs means `git clone` plus knowing the feature-flag matrix.
|
||||||
|
|
||||||
|
**Scope is deliberately one stage: produce binaries when a tag is
|
||||||
|
pushed, and attach them to a GitHub Release.** Channels, rollback,
|
||||||
|
update-in-place, signing, and §17's first-launch tool detection are all
|
||||||
|
**out of scope and named in §5**. This establishes a baseline, not the
|
||||||
|
arc.
|
||||||
|
|
||||||
|
**Revision 1 → 2**, all from review:
|
||||||
|
|
||||||
|
- **Q#D2 decided with a correction to revision 1's reasoning.** It is
|
||||||
|
`ubuntu-22.04` with an explicit **glibc ≥ 2.35** floor — and revision 1
|
||||||
|
was wrong to imply an older runner reaches RHEL 9. It does not (§1.6).
|
||||||
|
- **Q#D1 decided**, with acceptance strengthened to assert *both*
|
||||||
|
binaries report 1.1.0 (§1.3, acceptance 4).
|
||||||
|
- **The binary-exclusion mechanism is now two-layered**, not one:
|
||||||
|
explicit `--bin` targets *and* an explicit staged asset list (§1.2).
|
||||||
|
- **Revision 1 mis-stated `pmacs --gpu`'s failure mode**, and the
|
||||||
|
corrected version changes *why* co-location is required rather than
|
||||||
|
whether (§1.2a).
|
||||||
|
- **Both runners are pinned**, not just Linux (§1.8).
|
||||||
|
- **The RC is cut after merge from the merge SHA** and the final tag
|
||||||
|
reuses that same verified SHA (§7).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. Coherence impact (COHERENCE §20)
|
||||||
|
|
||||||
|
**Squarely §17 / §20 Priority 8: it completes journey step 1 without
|
||||||
|
adding an interaction island, a config surface, or a background-work
|
||||||
|
model.**
|
||||||
|
|
||||||
|
- **Journey step touched: step 1 (install)** — the one step no prior arc
|
||||||
|
could reach.
|
||||||
|
- **Concern: §17 Distribution**, graded "missing — zero release
|
||||||
|
machinery exists." This moves it to **Partial**, not Strong: binaries
|
||||||
|
on tag is the first of the seven things §17 asks for.
|
||||||
|
- **Interaction islands added:** none.
|
||||||
|
- **Config registry adoption:** none — this stage introduces no setting.
|
||||||
|
- **Background-work attribution:** none — no worker, job, or process.
|
||||||
|
- **Why now:** §20 P8 says every other priority's value is invisible
|
||||||
|
until this exists, and its prerequisite — a tree whose tests actually
|
||||||
|
run — was only satisfied by #209. Before that, a release would have
|
||||||
|
shipped from a corpus half of which CI had never compiled.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Ground truth (measured at `c5f7501`, 2026-08-01)
|
||||||
|
|
||||||
|
### 1.1 What exists
|
||||||
|
|
||||||
|
Nothing. One workflow, `ci.yml`, test-only; the only `release` strings
|
||||||
|
in it are `cargo test --release` flags.
|
||||||
|
|
||||||
|
### 1.2 The binary set is NOT what a glob would produce
|
||||||
|
|
||||||
|
`Cargo.toml` declares two `[[bin]]` targets, `pmacs` and `pmacs-audit`.
|
||||||
|
But **cargo also auto-discovers `src/bin/*.rs`**, so a release build
|
||||||
|
additionally produces `pmacs_fake_lsp` and `pmacs_fake_mcp` — test
|
||||||
|
fixtures whose purpose is to be spawned by acceptance suites. Plus
|
||||||
|
`pmacs-gpu` from its own package.
|
||||||
|
|
||||||
|
**Five binaries can land in `target/release`, and three must never
|
||||||
|
ship.**
|
||||||
|
|
||||||
|
| binary | ship? | why |
|
||||||
|
|---|---|---|
|
||||||
|
| `pmacs` | **yes** | the editor |
|
||||||
|
| `pmacs-gpu` | **yes** | the GPU frontend |
|
||||||
|
| `pmacs-audit` | **no** | audits pmacs source against the v1.0 lint rules — a contributor tool with no answer to "what is this for" |
|
||||||
|
| `pmacs_fake_lsp` | **never** | test fixture |
|
||||||
|
| `pmacs_fake_mcp` | **never** | test fixture |
|
||||||
|
|
||||||
|
**Exclusion is two-layered, deliberately.** Avoiding an upload glob is
|
||||||
|
not sufficient on its own:
|
||||||
|
|
||||||
|
1. **Build explicit `--bin` targets** — `--bin pmacs` and the
|
||||||
|
`pmacs-gpu` package — so the unwanted binaries are not produced by
|
||||||
|
the release build at all.
|
||||||
|
2. **Stage an explicit asset list** — copy named files into a staging
|
||||||
|
directory and archive *that*, so the archive's contents are a
|
||||||
|
decision rather than a directory's residue.
|
||||||
|
|
||||||
|
Layer 1 without layer 2 still archives whatever a cached
|
||||||
|
`target/release` happens to hold from an earlier step; layer 2 without
|
||||||
|
layer 1 relies on a list nobody re-checks when a new `src/bin/*.rs`
|
||||||
|
appears. Acceptance 2 asserts the **complete member list**, the
|
||||||
|
**executable bits**, and the **absence of all three** excluded binaries.
|
||||||
|
|
||||||
|
### 1.2b Layer 2 is load-bearing — demonstrated, not argued
|
||||||
|
|
||||||
|
Revision 2 justified the second layer with a hypothetical: "a cached
|
||||||
|
`target/release` can still hold binaries from an earlier build."
|
||||||
|
**Implementing the branch produced that case immediately.** After
|
||||||
|
running only
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo build --release --bin pmacs --features crdt
|
||||||
|
cargo build --release -p pmacs-gpu
|
||||||
|
```
|
||||||
|
|
||||||
|
on a tree where earlier work had run `cargo test --release` for the M10
|
||||||
|
perf gates, `target/release` contained:
|
||||||
|
|
||||||
|
```
|
||||||
|
pmacs pmacs-audit pmacs_fake_lsp pmacs_fake_mcp pmacs-gpu
|
||||||
|
```
|
||||||
|
|
||||||
|
**All three forbidden binaries were present**, left by the earlier test
|
||||||
|
build, despite this build naming only two targets. `Swatinem/rust-cache`
|
||||||
|
restores exactly this kind of directory in CI, so the risk is not
|
||||||
|
theoretical there either.
|
||||||
|
|
||||||
|
An implementation that took layer 1 as sufficient and archived
|
||||||
|
`target/release` would have published a fake language server in the
|
||||||
|
first release. The three archive assertions are bite-verified: a
|
||||||
|
smuggled `pmacs_fake_lsp`, a missing `pmacs-gpu`, and a cleared
|
||||||
|
executable bit are each caught, with the honest archive passing.
|
||||||
|
|
||||||
|
### 1.6a The glibc floor is asserted, not trusted
|
||||||
|
|
||||||
|
Acceptance 7 originally proposed verifying the floor by running the
|
||||||
|
binary in containers. **The shipped check is stronger and cheaper**:
|
||||||
|
read the versioned-symbol requirements straight out of the binary and
|
||||||
|
fail the build when any exceeds the floor.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
objdump -T <binary> | grep -oE 'GLIBC_[0-9]+\.[0-9]+' | sort -uV | tail -1
|
||||||
|
```
|
||||||
|
|
||||||
|
Why this beats the container test: it runs on **every** release rather
|
||||||
|
than once at RC time, it needs no network or images, and it fails at the
|
||||||
|
point of causation. Pinning `ubuntu-22.04` sets the floor but proves
|
||||||
|
nothing about the artifact — switching the job to `ubuntu-latest` would
|
||||||
|
otherwise ship binaries that fail to load with a bare `GLIBC_2.39 not
|
||||||
|
found` on a user's machine, with no clue which commit caused it. With
|
||||||
|
the assertion, that change fails in CI instead.
|
||||||
|
|
||||||
|
Bite-verified in both directions on a glibc 2.44 host, which stands in
|
||||||
|
for a mis-pinned runner: against a 2.35 floor both binaries are caught;
|
||||||
|
against a 2.44 floor both pass. Linux only — Mach-O has no equivalent
|
||||||
|
versioned-symbol scheme.
|
||||||
|
|
||||||
|
### 1.2a Why `pmacs` and `pmacs-gpu` must be co-located — corrected
|
||||||
|
|
||||||
|
Revision 1 said separating them makes `--gpu` "silently fail." **That is
|
||||||
|
wrong, and the real behaviour is better.** `gpu_binary`
|
||||||
|
(`src/main.rs:304`) prefers a **co-located** `pmacs-gpu` when that path
|
||||||
|
`is_file()`, otherwise falls back to the bare name `pmacs-gpu` for a
|
||||||
|
**PATH lookup**; if neither resolves, the error names both — *"sibling
|
||||||
|
… is absent and PATH lookup for pmacs-gpu failed"*.
|
||||||
|
|
||||||
|
So the requirement is not "otherwise it breaks quietly." It is that
|
||||||
|
**a release archive must be self-contained**: a user who unpacks it
|
||||||
|
somewhere not on `PATH` gets a working `--gpu` only if the two binaries
|
||||||
|
sit together. The fallback is a convenience for installed layouts, not a
|
||||||
|
substitute for shipping them as a unit.
|
||||||
|
|
||||||
|
### 1.3 The version is already incoherent, and a tag makes it visible
|
||||||
|
|
||||||
|
| crate | now | after |
|
||||||
|
|---|---|---|
|
||||||
|
| `pmacs` | 1.0.0 | **1.1.0** |
|
||||||
|
| `pmacs-gpu` | **0.0.1** | **1.1.0** |
|
||||||
|
| `pmacs-protocol` | 1.0.0 | **1.0.0** (unchanged) |
|
||||||
|
|
||||||
|
`pmacs-protocol` stays: it is the wire crate, carries publish-shaped
|
||||||
|
metadata, and versions on its own schedule — the protocol is v21 and
|
||||||
|
independent of the editor's release number.
|
||||||
|
|
||||||
|
Both shipped binaries report their own `env!("CARGO_PKG_VERSION")`:
|
||||||
|
`pmacs --version` at `src/main.rs:393`, and `pmacs-gpu --version` at
|
||||||
|
`pmacs-gpu/src/main.rs:667`, which prints `pmacs-gpu <ver> (protocol
|
||||||
|
v21)` — the protocol number is worth carrying into the release notes.
|
||||||
|
|
||||||
|
Tagging `v1.1.0` against the tree as it stands would publish a release
|
||||||
|
containing a `pmacs` reporting `1.0.0` and a `pmacs-gpu` reporting
|
||||||
|
`0.0.1`. **The workflow must refuse rather than publish** (acceptance
|
||||||
|
4), and acceptance asserts the *binaries'* output, not the manifests.
|
||||||
|
|
||||||
|
### 1.3a The bump exposed a defect: the daemon reported the wrong crate's version
|
||||||
|
|
||||||
|
**`InstanceIdentity::for_running_process` is defined in
|
||||||
|
`pmacs-protocol` and expanded `env!("CARGO_PKG_VERSION")` there.**
|
||||||
|
`env!` expands in the crate being *compiled*, so the field documented as
|
||||||
|
"Pmacs version string" carried the **protocol crate's** version.
|
||||||
|
|
||||||
|
This is not cosmetic. That identity reaches Lua as
|
||||||
|
`pmacs.instance.identity()` and goes on the wire in `Hello`, so every
|
||||||
|
attached frontend was told the daemon's version — and after the bump it
|
||||||
|
would have been told `1.0.0` by a `1.1.0` release.
|
||||||
|
|
||||||
|
**Nothing could have detected it before this stage.** Three tests assert
|
||||||
|
`id.pmacs_version == env!("CARGO_PKG_VERSION")` evaluated in the `pmacs`
|
||||||
|
crate, which is the correct assertion — but while both crates read
|
||||||
|
`1.0.0` they compared the same number reached by two different paths and
|
||||||
|
**could not fail**. Q#D1's decision to hold `pmacs-protocol` at 1.0.0
|
||||||
|
while moving `pmacs` is what made them discriminating, and all three
|
||||||
|
failed immediately on the bump.
|
||||||
|
|
||||||
|
The fix makes the version a **parameter**, so the `env!` expands in the
|
||||||
|
caller's crate; all three call sites are in `pmacs` and pass their own.
|
||||||
|
This is a breaking signature change to a `pub` function in
|
||||||
|
`pmacs-protocol`, which stays at 1.0.0 per Q#D1 — acceptable because the
|
||||||
|
crate is a path dependency with no external consumers, and recorded here
|
||||||
|
rather than silently absorbed.
|
||||||
|
|
||||||
|
*A test can be correct and still prove nothing, when the two things it
|
||||||
|
compares are equal for a reason unrelated to the code under test.*
|
||||||
|
|
||||||
|
### 1.4 The existing `v1.0.0` tag is stale and is not re-used
|
||||||
|
|
||||||
|
`v1.0.0` is pushed and points at `d3fa632` — the old release mirror's
|
||||||
|
head, **1,025 commits behind `main`**. There is also an `M8` milestone
|
||||||
|
tag. A `v*`-triggered workflow does not retroactively build for either,
|
||||||
|
and re-tagging would rewrite a published ref. The first real release is
|
||||||
|
**`v1.1.0`**, preceded by `v1.1.0-rc.1` (§7).
|
||||||
|
|
||||||
|
### 1.5 CRDT is not optional for a useful release
|
||||||
|
|
||||||
|
`default = ["luajit"]`; `luajit` and `lua54` are **mutually exclusive**;
|
||||||
|
`crdt` is opt-in on the root package, activated workspace-wide as
|
||||||
|
`pmacs/crdt`. README's documented build is the one to ship:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo build --release --workspace --features pmacs/crdt
|
||||||
|
```
|
||||||
|
|
||||||
|
**This is not a size/quality trade — a non-CRDT build refuses `--gpu`
|
||||||
|
outright.** `run_gpu` opens with a guard printing *"pmacs: --gpu
|
||||||
|
requires pmacs built with --features crdt"*. #209 established the same
|
||||||
|
thing on the wire side: `InstanceCapabilities::default` advertises
|
||||||
|
`multi_frontend` / `crdt_replica` / `semantic_render` only under the
|
||||||
|
feature. A non-CRDT release ships an editor that cannot use the GPU
|
||||||
|
frontend shipped beside it.
|
||||||
|
|
||||||
|
### 1.6 The glibc floor — decided, and revision 1's reasoning corrected
|
||||||
|
|
||||||
|
**Linux releases build on pinned `ubuntu-22.04`, with a stated support
|
||||||
|
floor of glibc ≥ 2.35.**
|
||||||
|
|
||||||
|
Revision 1 implied that moving off `ubuntu-latest` buys reach "several
|
||||||
|
distro generations" including RHEL 9. **It does not, and the arithmetic
|
||||||
|
matters:**
|
||||||
|
|
||||||
|
| target | glibc | covered by a 22.04 build? |
|
||||||
|
|---|---|---|
|
||||||
|
| Ubuntu 22.04 (jammy) | 2.35 | **yes** — this is the floor |
|
||||||
|
| Debian 12 (bookworm) | 2.36 | **yes** |
|
||||||
|
| Ubuntu 24.04 | 2.39 | yes |
|
||||||
|
| **RHEL 9** | **2.34** | **NO — below the floor** |
|
||||||
|
|
||||||
|
RHEL 9 is *older* than the floor, so it needs a lower-glibc container or
|
||||||
|
cross-build. **That is not a one-word runner change** and is parked
|
||||||
|
(§5). Stating the floor explicitly is what keeps this honest: a user on
|
||||||
|
RHEL 9 should read "not supported yet," not discover a loader error.
|
||||||
|
|
||||||
|
References: [runner images](https://github.com/actions/runner-images),
|
||||||
|
[jammy libc6](https://packages.ubuntu.com/jammy-updates/libc6),
|
||||||
|
[bookworm libc6](https://packages.debian.org/bookworm/libc6).
|
||||||
|
|
||||||
|
### 1.7 Runtime dependencies are documented but never checked
|
||||||
|
|
||||||
|
README names `/bin/sh`, `stty`, coreutils, git and tar, and tells
|
||||||
|
packagers to encode them. **Nothing checks any of them at runtime.**
|
||||||
|
Out of scope here (§5), but the release notes carry the list rather than
|
||||||
|
assume a downloader reads the README.
|
||||||
|
|
||||||
|
`pmacs-gpu` additionally needs a working Vulkan/Metal adapter; there is
|
||||||
|
no software-rasterizer fallback in a shipped binary.
|
||||||
|
|
||||||
|
### 1.8 Runner pinning, and what is NOT established
|
||||||
|
|
||||||
|
**Both runners are pinned**, not just Linux: `macos-latest` currently
|
||||||
|
resolves to macos-15 (ARM64) and **will drift** — the same silent-choice
|
||||||
|
problem as §1.6, in the other direction, where a future move could
|
||||||
|
change the minimum supported macOS without a commit to point at.
|
||||||
|
|
||||||
|
- **No release has ever been produced**, so nothing here is verified
|
||||||
|
end-to-end. Unlike #209 there is no existing behaviour to measure
|
||||||
|
against; the RC is the first evidence.
|
||||||
|
- **No Intel macOS** (§5). arm64 only.
|
||||||
|
- **No signing or notarization.** A downloaded macOS binary is
|
||||||
|
Gatekeeper-quarantined and needs an explicit override. Expected for an
|
||||||
|
unsigned baseline; it belongs in the release notes rather than in a
|
||||||
|
claim that macOS "just works".
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Decisions (all questions resolved at approval)
|
||||||
|
|
||||||
|
- **Q#D1 — versions.** `pmacs` and `pmacs-gpu` → 1.1.0;
|
||||||
|
`pmacs-protocol` stays 1.0.0. Acceptance asserts both shipped binaries
|
||||||
|
*report* 1.1.0 and the tag is `v1.1.0`.
|
||||||
|
- **Q#D2 — Linux runner.** Pinned `ubuntu-22.04`; support floor
|
||||||
|
glibc ≥ 2.35, covering Ubuntu 22.04 and Debian 12 but **not RHEL 9**.
|
||||||
|
- **Q#D3 — `pmacs-audit`.** Does not ship.
|
||||||
|
- **Q#D4 — Intel macOS.** Not in the baseline. macOS runner pinned.
|
||||||
|
- **Q#D5 — checksums.** Ship them.
|
||||||
|
- **Q#D6 — tests on release.** Do not re-run the suite; assert the
|
||||||
|
tagged commit is an ancestor of `main`, which catches the real mistake
|
||||||
|
(tagging a branch) at negligible cost.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Bets
|
||||||
|
|
||||||
|
- **Bet 1 — the release build is the CI build with different flags** and
|
||||||
|
will succeed first time.
|
||||||
|
- **Bet 2 — the artifact, not the build, is where this goes wrong.** The
|
||||||
|
plausible failures are packaging-shaped: a stray binary, a split
|
||||||
|
archive, a version mismatch, a glibc floor nobody notices.
|
||||||
|
- **Bet 3 — one RC is enough evidence.** A release either produces two
|
||||||
|
runnable, correctly-versioned binaries at a URL or it does not.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Acceptance
|
||||||
|
|
||||||
|
1. Pushing a tag matching `v*` produces a **GitHub Release** with
|
||||||
|
attached artifacts; no other trigger produces one. `v1.1.0-rc.1` is
|
||||||
|
marked **prerelease**.
|
||||||
|
2. Each archive's **complete member list** is asserted, along with
|
||||||
|
**executable bits**, and the **absence of `pmacs-audit`,
|
||||||
|
`pmacs_fake_lsp` and `pmacs_fake_mcp`**. Verified by listing the
|
||||||
|
downloaded archive, not by trusting the build command.
|
||||||
|
3. `pmacs` and `pmacs-gpu` are in the **same directory** within the
|
||||||
|
archive, so an unpacked release is self-contained (§1.2a).
|
||||||
|
4. **Version coherence, asserted from the binaries:** the workflow
|
||||||
|
refuses to publish when the tag disagrees with the root crate
|
||||||
|
version, and the downloaded `pmacs --version` and `pmacs-gpu
|
||||||
|
--version` both report **1.1.0**. Verified by a deliberate mismatch,
|
||||||
|
not by inspection.
|
||||||
|
5. The tagged commit is an ancestor of `main`.
|
||||||
|
6. Each artifact is a **CRDT build**, verified by running the shipped
|
||||||
|
binary rather than trusting the flag (§1.5).
|
||||||
|
7. The Linux binary honours the **glibc ≥ 2.35** floor, asserted from
|
||||||
|
the binary's own versioned symbols on every release (§1.6a) rather
|
||||||
|
than by a one-off container run, and the floor is stated in the
|
||||||
|
release notes and README.
|
||||||
|
8. `SHA256SUMS` covers every published artifact and verifies against the
|
||||||
|
downloads.
|
||||||
|
9. Release notes carry the runtime dependencies (§1.7), the glibc floor,
|
||||||
|
the arm64-only macOS scope, and the unsigned/Gatekeeper caveat.
|
||||||
|
10. `README.md`'s install section offers the download path **before**
|
||||||
|
from-source.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Parked (explicitly out of scope)
|
||||||
|
|
||||||
|
Named so they read as decisions. All are §17 requirements this stage
|
||||||
|
does not meet:
|
||||||
|
|
||||||
|
- **RHEL 9 and older glibc** — needs a container or cross-build (§1.6).
|
||||||
|
- **Intel macOS.**
|
||||||
|
- **Release channels**, stable/nightly.
|
||||||
|
- **Update-in-place and rollback.**
|
||||||
|
- **Signing and notarization.**
|
||||||
|
- **Reproducible builds.**
|
||||||
|
- **Package managers** — Homebrew, AUR, nixpkgs, distro packages.
|
||||||
|
- **Windows.** Unsupported anywhere in the tree today.
|
||||||
|
- **First-launch experience** — §17's config-directory creation and
|
||||||
|
optional-tool detection. That is §18 onboarding work.
|
||||||
|
- **Protocol/package-API compatibility reporting.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Gates
|
||||||
|
|
||||||
|
The standing `CLAUDE.md` suite applies unchanged; this stage adds no
|
||||||
|
Rust logic and no test, beyond the version bumps which the whole suite
|
||||||
|
covers. **Its real verification is the RC artifact** — acceptance 2, 3,
|
||||||
|
4, 6, 7 and 8 are all performed against a *downloaded* archive, because
|
||||||
|
that is the only thing that tests a release.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Branch plan
|
||||||
|
|
||||||
|
One branch, `distribution-stage1`:
|
||||||
|
|
||||||
|
1. **Bump `pmacs` and `pmacs-gpu` to 1.1.0** (Q#D1).
|
||||||
|
2. **Add `.github/workflows/release.yml`** — `on: push: tags: ['v*']`,
|
||||||
|
pinned `ubuntu-22.04` + pinned macOS runner, explicit `--bin`
|
||||||
|
targets, explicit staged asset list, the tag/version assertion, the
|
||||||
|
ancestor check, and `SHA256SUMS`.
|
||||||
|
3. **Update `README.md`** so download precedes build-from-source, and
|
||||||
|
state the glibc floor and macOS scope.
|
||||||
|
4. **Merge.**
|
||||||
|
5. **Tag `v1.1.0-rc.1` from the merge SHA**, marked prerelease. Verify
|
||||||
|
acceptance 2–8 against the published artifacts.
|
||||||
|
6. **Tag `v1.1.0` from that same verified SHA.**
|
||||||
|
|
||||||
|
**A tag before the merge does nothing, silently.** For `on: push:
|
||||||
|
tags`, GitHub resolves the workflow file **as it exists at the tagged
|
||||||
|
commit** — and it lists a repository's workflows from the *default
|
||||||
|
branch*, so `release.yml` is not even registered until this PR merges
|
||||||
|
(verified: `gh workflow list` shows only CI while the file lives on the
|
||||||
|
branch). Tagging any commit that predates the merge therefore produces
|
||||||
|
no run, no error, and no release. **A silent no-op is the worst possible
|
||||||
|
outcome for a release step, because it is indistinguishable from "not
|
||||||
|
started yet."** Cut the RC from the merge SHA and confirm a run actually
|
||||||
|
appeared before concluding anything about it.
|
||||||
|
|
||||||
|
**Steps 5 and 6 are the point of the ordering.** A tag on a branch would
|
||||||
|
publish a release from unmerged code, so the RC necessarily follows the
|
||||||
|
merge — and it is cut *from the merge SHA*, so the final tag can reuse
|
||||||
|
the exact commit the RC verified. **If the RC finds a defect, it is
|
||||||
|
fixed in a new PR and another RC is cut; the final tag is never the
|
||||||
|
first live execution of this workflow.**
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "pmacs-gpu"
|
name = "pmacs-gpu"
|
||||||
version = "0.0.1"
|
version = "1.1.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
rust-version = "1.95"
|
rust-version = "1.95"
|
||||||
description = "GPU/GUI frontend for pmacs — attach, editing, syntax/diagnostics/minimap, mouse + context menu, clipboard, search, minibuffer. See docs/pmacs-gpu-design.md."
|
description = "GPU/GUI frontend for pmacs — attach, editing, syntax/diagnostics/minimap, mouse + context menu, clipboard, search, minibuffer. See docs/pmacs-gpu-design.md."
|
||||||
|
|
|
||||||
|
|
@ -1885,13 +1885,30 @@ impl InstanceIdentity {
|
||||||
/// call site, so calling twice on different days surfaces different
|
/// call site, so calling twice on different days surfaces different
|
||||||
/// uptimes from the same anchor.
|
/// uptimes from the same anchor.
|
||||||
///
|
///
|
||||||
/// The version comes from `CARGO_PKG_VERSION` and the build hash
|
/// `pmacs_version` is supplied BY THE CALLER, and must be
|
||||||
/// from the optional `PMACS_GIT_HASH` environment variable populated
|
/// `env!("CARGO_PKG_VERSION")` evaluated in the `pmacs` crate.
|
||||||
/// by the build script.
|
///
|
||||||
|
/// It is a parameter rather than an `env!` here because `env!`
|
||||||
|
/// expands in the crate being COMPILED: reading it inside
|
||||||
|
/// `pmacs-protocol` yields the *protocol* crate's version, which
|
||||||
|
/// this field is not. The two crates held the same number until the
|
||||||
|
/// 1.1.0 release bump moved `pmacs` and left `pmacs-protocol` at
|
||||||
|
/// 1.0.0 — at which point the daemon began reporting the protocol
|
||||||
|
/// crate's version to every attached frontend, under a field named
|
||||||
|
/// `pmacs_version`. Nothing detected it earlier because the values
|
||||||
|
/// had always agreed by coincidence.
|
||||||
|
///
|
||||||
|
/// The build hash still comes from the optional `PMACS_GIT_HASH`
|
||||||
|
/// environment variable populated by the build script; that one is
|
||||||
|
/// genuinely crate-independent.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn for_running_process(instance_name: Option<String>, started: std::time::Instant) -> Self {
|
pub fn for_running_process(
|
||||||
|
pmacs_version: &str,
|
||||||
|
instance_name: Option<String>,
|
||||||
|
started: std::time::Instant,
|
||||||
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
pmacs_version: env!("CARGO_PKG_VERSION").into(),
|
pmacs_version: pmacs_version.into(),
|
||||||
build_hash: option_env!("PMACS_GIT_HASH").map(String::from),
|
build_hash: option_env!("PMACS_GIT_HASH").map(String::from),
|
||||||
instance_name,
|
instance_name,
|
||||||
uptime_secs: started.elapsed().as_secs(),
|
uptime_secs: started.elapsed().as_secs(),
|
||||||
|
|
|
||||||
|
|
@ -2393,6 +2393,7 @@ mod tests {
|
||||||
protocol_version: PROTOCOL_VERSION + 999,
|
protocol_version: PROTOCOL_VERSION + 999,
|
||||||
assigned_frontend_id: FrontendId::LOCAL,
|
assigned_frontend_id: FrontendId::LOCAL,
|
||||||
instance_identity: InstanceIdentity::for_running_process(
|
instance_identity: InstanceIdentity::for_running_process(
|
||||||
|
env!("CARGO_PKG_VERSION"),
|
||||||
None,
|
None,
|
||||||
std::time::Instant::now(),
|
std::time::Instant::now(),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -422,7 +422,11 @@ impl DaemonState {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_identity(&self) -> InstanceIdentity {
|
fn build_identity(&self) -> InstanceIdentity {
|
||||||
InstanceIdentity::for_running_process(self.instance_name.clone(), self.started)
|
InstanceIdentity::for_running_process(
|
||||||
|
env!("CARGO_PKG_VERSION"),
|
||||||
|
self.instance_name.clone(),
|
||||||
|
self.started,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `--socket NAME` value the daemon was launched with, or `None`
|
/// `--socket NAME` value the daemon was launched with, or `None`
|
||||||
|
|
|
||||||
|
|
@ -395,7 +395,11 @@ impl LocalInstanceInfo {
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn build_identity(&self) -> InstanceIdentity {
|
pub fn build_identity(&self) -> InstanceIdentity {
|
||||||
let data = self.0.borrow();
|
let data = self.0.borrow();
|
||||||
InstanceIdentity::for_running_process(data.name.clone(), data.started)
|
InstanceIdentity::for_running_process(
|
||||||
|
env!("CARGO_PKG_VERSION"),
|
||||||
|
data.name.clone(),
|
||||||
|
data.started,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue