name: release
on:
push:
tags: ["v*"]
pull_request:
paths:
- ".github/workflows/release.yml"
- "build.rs"
- "src/bundled.rs"
- "src/main.rs"
- "Cargo.toml"
- "Cargo.lock"
- "adapters/**"
- "base/**"
- "editors/**"
permissions:
contents: read
env:
CARGO_TERM_COLOR: always
jobs:
tag-matches-cargo-toml:
name: check tag (matches Cargo.toml)
if: startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
steps:
- name: check out the repo
uses: actions/checkout@v7
- name: compare tag and crate version
run: |
tagged="${GITHUB_REF_NAME#v}"
declared="$(cargo metadata --no-deps --format-version 1 | jq -r '.packages[0].version')"
if [ "$tagged" != "$declared" ]; then
echo "tag $GITHUB_REF_NAME says $tagged, Cargo.toml says $declared" >&2
exit 1
fi
echo "both say $declared"
tests:
name: run tests (at the tag)
if: startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
steps:
- name: check out the repo
uses: actions/checkout@v7
- name: restore the cargo cache
uses: Swatinem/rust-cache@v2
- name: run every test
run: cargo test --locked -- --include-ignored
- name: lint, with warnings as errors
run: cargo clippy --locked --all-targets -- -D warnings
build:
name: build (${{ matrix.target }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- target: x86_64-unknown-linux-musl
os: ubuntu-latest
- target: aarch64-unknown-linux-musl
os: ubuntu-24.04-arm
- target: x86_64-apple-darwin
os: macos-latest
- target: aarch64-apple-darwin
os: macos-latest
steps:
- name: check out the repo
uses: actions/checkout@v7
- name: install the musl toolchain
if: endsWith(matrix.target, '-musl')
run: sudo apt-get update && sudo apt-get install -y musl-tools
- name: add the target
run: rustup target add ${{ matrix.target }}
- name: restore the cargo cache
uses: Swatinem/rust-cache@v2
with:
key: ${{ matrix.target }}
- name: build the release binary
run: cargo build --release --locked --target ${{ matrix.target }}
- name: pack the tarball
run: |
staging="omh-${{ matrix.target }}"
mkdir -p "$staging"
cp "target/${{ matrix.target }}/release/omh" "$staging/"
cp LICENSE README.md "$staging/"
tar -czf "$staging.tar.gz" "$staging"
- name: upload the tarball
uses: actions/upload-artifact@v7
with:
name: omh-${{ matrix.target }}
path: omh-${{ matrix.target }}.tar.gz
if-no-files-found: error
runs-without-a-source-tree:
name: check artifact (runs standalone)
needs: build
runs-on: ubuntu-latest
steps:
- name: fetch the linux tarball
uses: actions/download-artifact@v8
with:
name: omh-x86_64-unknown-linux-musl
- name: unpack it outside any checkout
run: |
tar -xzf omh-x86_64-unknown-linux-musl.tar.gz
install -m755 omh-x86_64-unknown-linux-musl/omh "$RUNNER_TEMP/omh"
- name: run it in a fresh repo
env:
HOME: ${{ runner.temp }}/home
run: |
# The default runner shell is `bash -e`, without pipefail — so the
# `| tee` below would report tee's exit code and a failing init would
# pass. That is the failure mode this job exists to catch.
set -o pipefail
mkdir -p "$HOME"
repo="$(mktemp -d)"
cd "$repo"
git init -q -b main .
git config user.email ci@example.com
git config user.name ci
printf 'fn main() {}\n' > main.rs
git add -A && git commit -qm init
"$RUNNER_TEMP/omh" init
# Asserted against the filesystem, not against the report. A negative
# grep on a printed line ("harnesses 0") passes the moment that line
# is reworded, which is the failure mode CONTRIBUTING.md calls
# out under "assert invariants, not output shape". What matters is
# that the files arrived.
for dir in adapters base editors; do
count=$(find "$HOME/.omh/$dir" -name '*.toml' 2>/dev/null | wc -l)
if [ "$count" -eq 0 ]; then
echo "init installed no $dir — the bundled files did not travel" >&2
exit 1
fi
echo " $dir: $count"
done
release:
name: publish (github release)
needs: [tag-matches-cargo-toml, tests, build, runs-without-a-source-tree]
if: startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: collect every tarball
uses: actions/download-artifact@v8
with:
path: dist
merge-multiple: true
- name: checksum every tarball
run: |
cd dist
# A release missing a platform would otherwise ship with a
# SHA256SUMS that agrees with itself perfectly.
found=$(ls -1 ./*.tar.gz | wc -l)
if [ "$found" -ne 4 ]; then
echo "expected 4 tarballs, found $found" >&2
ls -la >&2
exit 1
fi
sha256sum *.tar.gz > SHA256SUMS
cat SHA256SUMS
- name: create the release
env:
GH_TOKEN: ${{ github.token }}
run: |
gh release create "$GITHUB_REF_NAME" \
--repo "$GITHUB_REPOSITORY" \
--title "$GITHUB_REF_NAME" \
--generate-notes \
--verify-tag \
dist/*.tar.gz dist/SHA256SUMS
crates-io:
name: publish (crates.io)
needs: [release]
if: startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
steps:
- name: check out the repo
uses: actions/checkout@v7
- name: restore the cargo cache
uses: Swatinem/rust-cache@v2
- name: cargo publish
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
run: cargo publish --locked
homebrew:
name: publish (homebrew tap)
needs: [release]
if: startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
steps:
- name: check out the repo
uses: actions/checkout@v7
- name: download the release checksums
env:
GH_TOKEN: ${{ github.token }}
run: |
gh release download "$GITHUB_REF_NAME" --repo "$GITHUB_REPOSITORY" \
--pattern SHA256SUMS --dir .
cat SHA256SUMS
- name: render the formula
run: |
set -o pipefail
version="${GITHUB_REF_NAME#v}"
sha_for() {
awk -v f="omh-$1.tar.gz" '{ sub(/^\.\//, "", $2); if ($2 == f) print $1 }' SHA256SUMS
}
formula=$(cat packaging/homebrew/omh.rb.tmpl)
formula=${formula//@VERSION@/$version}
for pair in \
"SHA_DARWIN_ARM:aarch64-apple-darwin" \
"SHA_DARWIN_X86:x86_64-apple-darwin" \
"SHA_LINUX_ARM:aarch64-unknown-linux-musl" \
"SHA_LINUX_X86:x86_64-unknown-linux-musl"
do
key="${pair%%:*}"; target="${pair#*:}"
sum="$(sha_for "$target")"
if [ -z "$sum" ]; then
echo "no checksum for $target in the release — refusing to write a formula that cannot install" >&2
exit 1
fi
formula=${formula//@$key@/$sum}
done
# A placeholder that survives means the template grew a field the
# renderer does not know about, and it would reach users as a Ruby
# syntax error at install time.
if printf '%s' "$formula" | grep -q '@[A-Z_]\+@'; then
echo "unfilled placeholders remain:" >&2
printf '%s' "$formula" | grep -o '@[A-Z_]\+@' | sort -u >&2
exit 1
fi
# The newline is not cosmetic: `brew style` fails a formula without a
# final one, so writing it with a bare `printf '%s'` would turn the
# tap's own audit red on the first real release.
printf '%s\n' "$formula" > omh.rb
ruby -c omh.rb
- name: check out the tap
uses: actions/checkout@v7
with:
repository: mindsers/homebrew-tap
token: ${{ secrets.HOMEBREW_TAP_TOKEN }}
path: tap
- name: commit and push to the tap
run: |
version="${GITHUB_REF_NAME#v}"
mkdir -p tap/Formula
cp omh.rb tap/Formula/omh.rb
cd tap
git config user.name "omh release"
git config user.email "nathanael@cherrier.dev"
# Staged first, and compared against the index rather than the
# working tree. `git diff` ignores untracked files, so on the release
# that first created this formula it reported no change, the no-op
# branch below fired, and the job exited 0 having published nothing —
# a green tick over an empty tap. The one case that had to work was
# the one case the guard could not see.
git add Formula/omh.rb
if git diff --cached --quiet -- Formula/omh.rb; then
echo "the tap already describes $version"
exit 0
fi
git commit -m "omh $version
Rendered from packaging/homebrew/omh.rb.tmpl at $GITHUB_REF_NAME.
Checksums are the ones that release published."
git push
# Read it back from the remote. Everything above can succeed against
# a local clone and still leave the tap untouched, which is exactly
# what happened once.
git fetch -q origin
git diff --quiet "origin/$(git branch --show-current)" -- Formula/omh.rb \
|| { echo "the push did not land Formula/omh.rb" >&2; exit 1; }
echo "the tap now describes $version"
packaging:
name: check crate (packages cleanly)
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- name: check out the repo
uses: actions/checkout@v7
- name: restore the cargo cache
uses: Swatinem/rust-cache@v2
- name: package the crate
run: cargo package --locked
- name: list what would ship
run: cargo package --locked --list