Skip to content

release

release #872

Workflow file for this run

name: release
# Cut a release by pushing a tag, e.g.
# git tag v0.1.0 && git push origin v0.1.0
#
# This builds the `stella` binary for every target in the matrix, packages a
# `stella-<version>-<target>.tar.gz` (binary + LICENSE/NOTICE/LICENSING +
# README), and — on a tag push — publishes a GitHub Release with all tarballs
# and a SHA256SUMS.
#
# This is a hand-rolled matrix workflow: it does NOT require cargo-dist to be
# installed on the runner. A future maintainer can migrate to cargo-dist using
# the `[workspace.metadata.dist]` section already declared in Cargo.toml.
on:
push:
tags:
- "v*"
workflow_dispatch:
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false
env:
CARGO_TERM_COLOR: always
permissions:
contents: read
jobs:
build:
name: build ${{ matrix.target }}
runs-on: ${{ matrix.os }}
# Release builds use full LTO + codegen-units=1 (see [profile.release] in
# Cargo.toml) and compile bundled C/C++ deps (rusqlite, tree-sitter), so
# give each build room on a cold cache.
timeout-minutes: 90
strategy:
fail-fast: false
matrix:
include:
- target: aarch64-apple-darwin
os: macos-14
# Cross-compiled from the Apple Silicon runner: Apple's clang/ld is a
# universal toolchain, so `rustup target add x86_64-apple-darwin`
# (via the `targets:` input below) builds the x86_64 slice — bundled
# C deps included — without needing a (now-retired) Intel runner.
- target: x86_64-apple-darwin
os: macos-14
- target: x86_64-unknown-linux-gnu
os: ubuntu-latest
# Built natively on GitHub's arm64 runner (free for public repos).
# Cross-compiling this target via `cross` fails: its default docker
# image ships g++ 5, far too old for the bundled C/C++ this workspace
# compiles. (The dep that first hit this, libduckdb-sys, is no longer
# in the graph — `rg '^name = "libduckdb-sys"' Cargo.lock` finds
# nothing — but rusqlite's bundled SQLite and the tree-sitter grammars
# keep the constraint alive.) The native runner uses the same modern
# toolchain that already builds them on x86_64 ubuntu-latest.
- target: aarch64-unknown-linux-gnu
os: ubuntu-24.04-arm
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- name: Install Rust stable
uses: dtolnay/rust-toolchain@4cda84d5c5c54efe2404f9d843567869ab1699d4 # stable
with:
targets: ${{ matrix.target }}
- uses: Swatinem/rust-cache@f0d9c3887740aee45f6153b24b3a6b815192ec16 # v2
with:
key: ${{ matrix.target }}
# rust-toolchain.toml pins the repo to a concrete toolchain (e.g. 1.97.0),
# and the build runs on *that*, not `stable`. dtolnay's `targets:` above
# installs the target for `stable`, so a genuine cross-target (x86_64 on
# the arm64 macOS runner) fails with E0463 "can't find crate for `core`".
# Add the target to the pinned toolchain — this runs in the checkout, so
# rustup honors rust-toolchain.toml (auto-installing the pin on first use).
- name: Add the cross target to the pinned toolchain
run: rustup target add ${{ matrix.target }}
# The tagged tree carries its own version since #786 — auto-tag.yml
# stamps the manifests in the release commit the tag points at, so this
# step only VERIFIES the manifest matches the tag instead of rewriting
# it. A mismatch means the tag predates #786 (or auto-tag regressed);
# failing loud beats shipping a binary that reports the wrong version.
- name: Verify the tagged tree carries its own version
if: ${{ github.ref_type == 'tag' }}
shell: bash
run: |
set -euo pipefail
version="${GITHUB_REF_NAME#v}"
grep -m1 '^version = ' Cargo.toml | grep -q "\"${version}\"" \
|| { echo "::error::Cargo.toml workspace version does not match tag ${GITHUB_REF_NAME} — the tag was not cut by the current auto-tag.yml (see #786)."; exit 1; }
# Every target builds natively: the two Apple targets share one universal
# Apple toolchain, and aarch64-linux builds on ubuntu-24.04-arm. `cross`
# was tried and abandoned (see the matrix comments above) and is installed
# by no step here.
#
# The build goes through scripts/repro-build.sh rather than calling cargo
# inline, and that indirection is the whole of #910: the script is the one
# place that remaps $CARGO_HOME and the rustup sysroot out of the emitted
# paths (a release binary used to carry 553 absolute builder paths, so its
# SHA-256 described the builder's home directory rather than the source),
# asserts that the rust-toolchain.toml pin is what actually ran, exports
# SOURCE_DATE_EPOCH for the packaging step, and writes the per-target
# binary checksum. `--locked` is preserved from #786: the tagged tree's
# lockfile is synced to its own version, same as install.sh's from-source
# build. scripts/check-repro-wiring.sh fails if this step ever goes back
# to a bare `cargo build`.
- name: Build stella (reproducible)
shell: bash
run: ./scripts/repro-build.sh --locked "${{ matrix.target }}"
- name: Package tarball
id: package
shell: bash
run: |
set -euo pipefail
if [ "${GITHUB_REF_TYPE}" = "tag" ]; then
version="${GITHUB_REF_NAME#v}"
else
# workflow_dispatch (no tag): fall back to the workspace version.
version="$(sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml | head -n1)"
fi
target="${{ matrix.target }}"
stem="stella-${version}-${target}"
bin="target/${target}/release/stella"
mkdir -p "dist/${stem}"
cp "${bin}" "dist/${stem}/stella"
# AGPL §4/§5: the license text ships with every distributed copy.
cp LICENSE NOTICE LICENSING.md README.md "dist/${stem}/"
# Not `tar -czf`: tar records mtimes, uid/gid and traversal order and
# gzip stamps its header with the current time, so two runners that
# produce a byte-identical *binary* still produce different
# *tarballs* — and SHA256SUMS, which is what install.sh and the
# Homebrew formula check, would diverge anyway. The macOS runners
# have bsdtar and the Linux ones GNU tar, whose determinism flags do
# not overlap, hence a script rather than flags.
SOURCE_DATE_EPOCH="$(git log -1 --pretty=%ct)"
export SOURCE_DATE_EPOCH
./scripts/package-tarball.py "dist/${stem}" "${stem}.tar.gz"
# The bare-binary checksum, written by repro-build.sh in
# `sha256sum` format with the artifact stem as its name column, so
# the four of them concatenate into SHA256SUMS.bin below. This is the
# number an independent rebuilder compares against — the tarball hash
# cannot serve that purpose, since rebuilding gives them a binary and
# not our archive of it.
cp "target/${target}/release/stella.sha256" "${stem}.sha256"
cat "${stem}.sha256"
echo "asset=${stem}.tar.gz" >> "$GITHUB_OUTPUT"
echo "sums=${stem}.sha256" >> "$GITHUB_OUTPUT"
# Uploaded twice, because one unlucky DNS lookup used to throw away a
# finished release. v0.9.273 compiled for seven minutes, packaged its
# tarball, and then died here on `Failed to CreateArtifact: Unable to
# make request: ENOTFOUND` — an Actions endpoint the runner could not
# resolve. Every downstream job needs all four artifacts, so that one
# step took the whole tag with it (#5629).
#
# `continue-on-error` on the first step is what lets a second one run;
# the retry itself is a plain step, so a real upload failure still fails
# the job. No retry action is added: a third-party action would owe
# `action-pins` a pinned SHA and the licence allowlist a review, to buy
# one `if:` condition.
- name: Upload build artifact
id: upload
continue-on-error: true
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: stella-${{ matrix.target }}
path: |
${{ steps.package.outputs.asset }}
${{ steps.package.outputs.sums }}
if-no-files-found: error
retention-days: 7
# `overwrite` covers the narrower failure where the first attempt got far
# enough to reserve the name before it broke; without it the retry would
# fail on a name conflict of its own making.
- name: Upload build artifact (retry)
if: steps.upload.outcome == 'failure'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: stella-${{ matrix.target }}
path: |
${{ steps.package.outputs.asset }}
${{ steps.package.outputs.sums }}
if-no-files-found: error
retention-days: 7
overwrite: true
# Rebuild one target on a second runner and refuse to publish if the bytes
# differ (#910, acceptance clause). This is the check that keeps
# reproducibility fixed; scripts/check-repro-wiring.sh only keeps the plumbing
# in place.
#
# THE VARIATION IS THE TEST. Two GitHub-hosted runners of the same image share
# $HOME=/home/runner and $CARGO_HOME=/home/runner/.cargo, so the obvious
# reading of "build twice on different runners and diff the SHA" would have
# passed green on the *unfixed* code and proved nothing — a permanently-green
# gate is worse than no gate. So this arm deliberately differs from the build
# job in every host-shaped input that used to leak into the binary:
#
# CARGO_HOME different -> exercises the $CARGO_HOME remap (506 of the
# 553 absolute paths a release binary used to carry)
# RUSTUP_HOME different -> exercises the sysroot remap (the other 47)
# rust-src installed -> the subtle one. rustc emits std's paths as
# the virtual /rustc/<hash>/… but translates them back to the
# real sysroot path when rust-src is present, so the same
# source on the same pinned toolchain produces different
# bytes depending on whether the builder happens to have the
# component. The build job does not install it; this one does.
# checkout path different -> nothing should depend on the workspace root
# TMPDIR different -> nor on where the bundled C deps compile
# no rust-cache -> nothing is restored from the first arm
#
# One linux target, not four: this doubles a full-LTO build (budgeted at 90
# minutes) and every target shares the same remapping. Runs on the tag push
# that would publish the artifact, where a divergence blocks the release, and
# on workflow_dispatch for checking between releases. Deliberately NOT on
# pull_request — an hour of LTO per PR is not a cost this repo should carry
# for a property that can only change in release.yml or repro-build.sh, both
# of which the toolchain-free wiring guard already watches on every PR.
#
# `needs: build` costs a release one extra build's wall time, because this arm
# cannot start until the first arm's checksum artifact exists. That is the
# deliberate trade: running the two in parallel and comparing inside the
# `release` job would be faster, but then no job owns the verdict and a green
# "reproducible build" check would mean only that a build finished.
verify-reproducible:
name: reproducible build (independent rebuild)
needs: build
runs-on: ubuntu-latest
timeout-minutes: 90
env:
REPRO_TARGET: x86_64-unknown-linux-gnu
steps:
# Before the toolchain step, not after: rustup installs toolchains into
# RUSTUP_HOME and shims into CARGO_HOME at the moment it runs, so setting
# these later would leave the arm building out of the runner's default
# directories and silently make this job a copy of the first one.
- name: Move cargo, rustup and TMPDIR somewhere the first arm never used
run: |
set -euo pipefail
mkdir -p "${RUNNER_TEMP}/arm-b/tmp"
{
echo "CARGO_HOME=${RUNNER_TEMP}/arm-b/cargo"
echo "RUSTUP_HOME=${RUNNER_TEMP}/arm-b/rustup"
echo "TMPDIR=${RUNNER_TEMP}/arm-b/tmp"
} >> "$GITHUB_ENV"
echo "${RUNNER_TEMP}/arm-b/cargo/bin" >> "$GITHUB_PATH"
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
path: arm-b
- name: Install Rust stable
uses: dtolnay/rust-toolchain@4cda84d5c5c54efe2404f9d843567869ab1699d4 # stable
with:
targets: x86_64-unknown-linux-gnu
# Inside the checkout, so rustup honors rust-toolchain.toml and installs
# the pin (not `stable`). rust-src is the deliberate difference described
# above; repro-build.sh refuses to run on anything but the pin, which is
# also how #910 point 3 ("confirm the release job uses the pinned
# toolchain") stops being a comment and becomes a check.
- name: Install the pinned toolchain WITH rust-src
working-directory: arm-b
run: |
set -euo pipefail
rustup component add rust-src
rustc -vV
- name: Rebuild
working-directory: arm-b
run: ./scripts/repro-build.sh --locked "${REPRO_TARGET}"
- name: Download the first arm's checksum
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: stella-x86_64-unknown-linux-gnu
path: arm-a
- name: Compare
run: |
set -euo pipefail
a="$(cat arm-a/*.sha256)"
b="$(cat "arm-b/target/${REPRO_TARGET}/release/stella.sha256")"
echo "arm A (build job): ${a}"
echo "arm B (this job): ${b}"
if [ "$a" != "$b" ]; then
echo "::error::release build is NOT reproducible — two runners produced different bytes for ${REPRO_TARGET}."
echo "Arm B differs from arm A only in CARGO_HOME, RUSTUP_HOME, TMPDIR, the checkout path and the presence of rust-src." >&2
echo "Something the build reads from the host is still reaching the binary. Compare with:" >&2
echo " strings <binary> | grep -E '^/(home|Users|tmp)/'" >&2
exit 1
fi
echo "reproducible: both arms produced ${b%% *}"
# Execute the artifact this release would publish (#1626). Nothing else in
# this pipeline ever did: `verify-reproducible` above proves the build is
# deterministic, which a binary that segfaults on startup satisfies
# perfectly, and `ci.yml` triggers on pull_request and pushes to main — not
# on tags. A release could therefore go green, reproducible, signed,
# attested and out to the Homebrew tap without a single process having run
# it. The first execution of a published stella artifact by anyone was by
# hand, in an issue.
#
# `needs: build` and downloading its *uploaded* artifact rather than reading
# a path in the build tree: a build-tree binary would not prove the tarball
# unpacks, keeps its mode bits, or contains the binary whose checksum ships
# in SHA256SUMS.bin. This tests what a user receives.
#
# Two of the four targets, because those are the two a GitHub runner can
# execute natively — an aarch64-linux or x86_64-darwin artifact would need
# emulation, and a smoke gate that lies about which bytes it ran is worse
# than one that admits a gap. `fail-fast: false` so a macOS-only break is
# reported as macOS-only.
#
# The assertions live in scripts/smoke-artifact.sh, not inline here: that
# puts them under `make shellcheck` and gives them a witness suite
# (scripts/test-smoke-artifact.sh) that drives every failure branch against
# a synthetic broken artifact. An inline `run:` block is the shape that
# cannot be tested until a release is already going out.
smoke:
name: smoke ${{ matrix.target }}
needs: build
runs-on: ${{ matrix.os }}
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
include:
- target: aarch64-apple-darwin
os: macos-14
- target: x86_64-unknown-linux-gnu
os: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- name: Download the artifact this release would publish
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: stella-${{ matrix.target }}
path: published
- name: Unpack it, verify it against its published checksum, and run it
run: ./scripts/smoke-artifact.sh published
release:
name: publish GitHub Release
# `smoke` gates this rather than the `homebrew` job downstream: a release
# that cannot run is not one to publish tarballs for either, and gating
# here covers the tap transitively.
needs: [build, verify-reproducible, smoke]
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/')
permissions:
contents: write
# Provenance attestation (#649). `id-token: write` mints the short-lived
# OIDC token Sigstore signs against; `attestations: write` stores the
# resulting bundle on the repo. No signing key is involved, so there is
# no key to rotate or leak — which is what made this landable.
id-token: write
attestations: write
steps:
# Full history + tags so the notes step can diff against the prior tag.
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
fetch-depth: 0
fetch-tags: true
- name: Download all build artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
path: artifacts
pattern: stella-*
merge-multiple: true
# Two sums files, because they answer two different questions.
#
# SHA256SUMS hashes the tarballs. It is what install.sh and the
# Homebrew formula check: "the thing I downloaded is the
# thing that was published".
# SHA256SUMS.bin hashes the bare binaries, one line per target, as
# written by scripts/repro-build.sh. It is what an
# independent rebuilder checks: rebuilding the tag gives
# them a binary, not our archive of it, so without this
# file the reproducibility work is unverifiable from
# outside no matter how reproducible the build became
# (#910 point 5). RELEASING.md documents the recipe.
- name: Generate SHA256SUMS
shell: bash
run: |
set -euo pipefail
cd artifacts
sha256sum stella-*.tar.gz > SHA256SUMS
# Sorted by the stem in the name column so the file's own bytes do
# not depend on which target's job finished first.
cat stella-*.sha256 | sort -k2 > SHA256SUMS.bin
echo "----- SHA256SUMS -----"
cat SHA256SUMS
echo "----- SHA256SUMS.bin (bare binaries) -----"
cat SHA256SUMS.bin
# Attest the tarballs *and* SHA256SUMS. Attesting the sums file is the
# point: install.sh trusts it to vouch for the tarball, but it was
# fetched over the same channel from the same release, so anything able
# to replace one could replace the other. A provenance bundle is bound to
# this workflow at this commit and cannot be reissued by whoever holds
# the release.
#
# Pinned by SHA because that is the invariant #648 established; adding a
# floating ref here would be the first thing to break its CI guard.
- name: Attest build provenance
uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2
with:
subject-path: |
artifacts/stella-*.tar.gz
artifacts/SHA256SUMS
artifacts/SHA256SUMS.bin
# Draft the release notes from the diff since the previous tag using the
# AI Gateway. Falls back to a plain commit list if the key is unset or the
# call fails, so a release is never blocked on note generation.
- name: Generate release notes
shell: bash
env:
AI_GATEWAY_API_KEY: ${{ secrets.AI_GATEWAY_API_KEY }}
RELEASE_NOTES_MODEL: ${{ vars.RELEASE_NOTES_MODEL || 'anthropic/claude-sonnet-5' }}
run: |
set -euo pipefail
tag="${GITHUB_REF_NAME}"
version="${tag#v}"
prev="$(git tag -l 'v*' --sort=-v:refname | grep -vx "${tag}" | head -n1 || true)"
echo "notes range: ${prev:-<start>}..${tag}"
commits="$(git log --no-merges --pretty='- %s' ${prev:+${prev}..HEAD} | head -n 300)"
{
echo "## Commits"; echo "$commits";
echo; echo "## Files changed";
git diff --stat ${prev:+${prev}..HEAD} | tail -n 80;
} > ctx.txt
git diff ${prev:+${prev}..HEAD} 2>/dev/null | head -c 120000 > diff.txt || true
notes=""
if [ -n "${AI_GATEWAY_API_KEY:-}" ]; then
prompt="$(printf 'Write the GitHub release notes for the "stella" coding-agent CLI %s, in clean Markdown. Start with a one-line summary, then grouped bullet sections (Features, Fixes, Performance, Docs, Internal) — omit any empty section. Be concrete and user-facing; no preamble, no headings above the summary. Base it strictly on the commits and diff below.\n\n%s\n\n## Diff (truncated)\n```diff\n%s\n```\n' "$version" "$(cat ctx.txt)" "$(cat diff.txt)")"
payload="$(jq -n --arg m "$RELEASE_NOTES_MODEL" --arg c "$prompt" '{model:$m,messages:[{role:"user",content:$c}],temperature:0.2}')"
resp="$(curl -sS --max-time 150 \
-H "Authorization: Bearer ${AI_GATEWAY_API_KEY}" \
-H "Content-Type: application/json" \
-d "$payload" \
https://ai-gateway.vercel.sh/v1/chat/completions || true)"
notes="$(printf '%s' "$resp" | jq -r '.choices[0].message.content // empty' 2>/dev/null || true)"
[ -z "$notes" ] && echo "::warning::AI note generation returned nothing; using the commit list."
else
echo "::warning::AI_GATEWAY_API_KEY not available; using the commit list."
fi
[ -z "$notes" ] && notes="$(printf '## What changed\n\n%s' "$commits")"
{
printf '%s\n\n' "$notes"
[ -n "$prev" ] && printf '**Full changelog**: https://github.com/%s/compare/%s...%s\n' "$GITHUB_REPOSITORY" "$prev" "$tag"
} > notes.md
echo "----- notes.md -----"; cat notes.md
# A draft release is not attached to its tag, so `GET /releases/tags/{tag}`
# answers 404 for one and the publish step below creates a second release
# instead of finishing the first. That is how v0.9.254 and v0.9.264 ended
# up as drafts nothing would ever touch again: the step created the
# release, uploaded its assets, then hit a GitHub 5xx before publishing,
# and a re-run would have left the half-built draft sitting beside a new
# one (#5629). Deleting it first is what makes a re-run finish the job.
#
# Only a draft is ever deleted. A published release for this tag is a
# release users can already download, and this step must never touch it.
- name: Delete a stale draft release for this tag
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
shell: bash
run: |
set -euo pipefail
tag="${GITHUB_REF_NAME}"
# `--slurp` cannot be combined with `--jq` (gh rejects that), so the
# filter runs in a plain jq after the fetch.
ids="$(CLICOLOR_FORCE=0 NO_COLOR=1 gh api --paginate --slurp \
'repos/{owner}/{repo}/releases?per_page=100' \
| jq -r --arg tag "$tag" '.[][] | select(.draft == true and .tag_name == $tag) | .id')"
if [ -z "$ids" ]; then
echo "no draft release for ${tag}; nothing to clear."
exit 0
fi
for id in $ids; do
echo "::notice::deleting stale draft release ${id} for ${tag} so this run can publish."
gh api -X DELETE "repos/{owner}/{repo}/releases/${id}"
done
- name: Create / update GitHub Release
uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3
with:
files: |
artifacts/stella-*.tar.gz
artifacts/SHA256SUMS
artifacts/SHA256SUMS.bin
fail_on_unmatched_files: true
body_path: notes.md
# Renders .github/homebrew/stella.rb.tmpl with the real version + per-target
# SHA-256 sums and commits it as Formula/stella.rb to the tap repo
# (macanderson/homebrew-tap). Runs only after the GitHub Release exists, so
# the formula's release-asset URLs resolve. Auth: HOMEBREW_TAP_DEPLOY_KEY
# (an SSH deploy key with write access to the tap repo — scoped to exactly
# that one repo, unlike a PAT), with HOMEBREW_TAP_TOKEN (https) honored as a
# fallback. Skips gracefully when neither is set so a release is never
# blocked on the tap being wired.
homebrew:
name: publish Homebrew formula
needs: release
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/')
steps:
- name: Checkout (for the formula template)
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- name: Download all build artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
path: artifacts
pattern: stella-*
merge-multiple: true
- name: Render formula and push to tap
env:
HOMEBREW_TAP_DEPLOY_KEY: ${{ secrets.HOMEBREW_TAP_DEPLOY_KEY }}
HOMEBREW_TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
shell: bash
run: |
set -euo pipefail
if [ -z "${HOMEBREW_TAP_DEPLOY_KEY:-}" ] && [ -z "${HOMEBREW_TAP_TOKEN:-}" ]; then
echo "::warning::neither HOMEBREW_TAP_DEPLOY_KEY nor HOMEBREW_TAP_TOKEN is set — skipping Homebrew tap publish."
echo "One-time setup (create the tap repo + add the credential) is documented in RELEASING.md."
exit 0
fi
version="${GITHUB_REF_NAME#v}"
if [ -n "${HOMEBREW_TAP_DEPLOY_KEY:-}" ]; then
# SSH with the tap-scoped deploy key. The key file lives outside
# the workspace and is chmod 600, as ssh requires.
keyfile="${RUNNER_TEMP}/tap_deploy_key"
printf '%s\n' "${HOMEBREW_TAP_DEPLOY_KEY}" > "${keyfile}"
chmod 600 "${keyfile}"
export GIT_SSH_COMMAND="ssh -i ${keyfile} -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new"
git clone --depth 1 "git@github.com:macanderson/homebrew-tap.git" tap
else
git clone --depth 1 \
"https://x-access-token:${HOMEBREW_TAP_TOKEN}@github.com/macanderson/homebrew-tap.git" tap
fi
# The tap is cloned before anything is rendered so this job can read
# the version it currently serves and refuse to go backwards.
#
# Re-running release.yml on an old tag checks out the workflow AS OF
# THAT TAG and renders the formula for that version unconditionally,
# so retrying a months-old failed release would hand every `brew
# upgrade` an older stella than the one it has (#5629). The formula
# is the one artifact here that is a pointer rather than an addition:
# a stale GitHub Release is harmless beside the newer ones, and a
# stale Formula/stella.rb is what everybody installs.
tap_version=""
if [ -f tap/Formula/stella.rb ]; then
tap_version="$(sed -n 's/^[[:space:]]*version "\([^"]*\)".*/\1/p' tap/Formula/stella.rb | head -n1)"
fi
if [ -n "${tap_version}" ] && [ "${tap_version}" != "${version}" ]; then
older="$(printf '%s\n%s\n' "${version}" "${tap_version}" | sort -V | head -n1)"
if [ "${older}" = "${version}" ]; then
echo "::notice::the tap serves ${tap_version}; not rendering the older ${version} over it."
exit 0
fi
fi
# SHA-256 of a given target's release tarball (same bytes uploaded to
# the GitHub Release in the `release` job).
sha_for() {
local target="$1"
local f="artifacts/stella-${version}-${target}.tar.gz"
if [ ! -f "$f" ]; then
echo "::error::expected release asset not found: ${f}" >&2
exit 1
fi
sha256sum "$f" | awk '{ print $1 }'
}
sha_aarch64_darwin="$(sha_for aarch64-apple-darwin)"
sha_x86_64_darwin="$(sha_for x86_64-apple-darwin)"
sha_aarch64_linux="$(sha_for aarch64-unknown-linux-gnu)"
sha_x86_64_linux="$(sha_for x86_64-unknown-linux-gnu)"
mkdir -p rendered
sed \
-e "s/@VERSION@/${version}/g" \
-e "s/@SHA_AARCH64_DARWIN@/${sha_aarch64_darwin}/g" \
-e "s/@SHA_X86_64_DARWIN@/${sha_x86_64_darwin}/g" \
-e "s/@SHA_AARCH64_LINUX@/${sha_aarch64_linux}/g" \
-e "s/@SHA_X86_64_LINUX@/${sha_x86_64_linux}/g" \
.github/homebrew/stella.rb.tmpl > rendered/stella.rb
echo "----- rendered Formula/stella.rb -----"
cat rendered/stella.rb
mkdir -p tap/Formula
cp rendered/stella.rb tap/Formula/stella.rb
cd tap
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add Formula/stella.rb
if git diff --cached --quiet; then
echo "Formula already up to date for ${version}; nothing to commit."
exit 0
fi
git commit -m "stella ${version}"
git push origin HEAD