Skip to content

release: 0.21.15

release: 0.21.15 #130

Workflow file for this run

name: Release
on:
push:
tags:
- "v*"
permissions:
id-token: write
attestations: write
contents: write
jobs:
sbom:
name: Generate SBOM
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Install Rust
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: stable
# Install from crates.io rather than psastras/sbom-rs/actions/…@cargo-sbom-latest:
# that tag is stale (2023, no actions/ dir) so the action ref fails to resolve.
- name: Install cargo-sbom
run: cargo install cargo-sbom --locked
# cargo-sbom writes to stdout (no -o flag) and the SPDX value is
# spdx_json_2_3 (not spdx_2_3).
- name: Generate SPDX SBOM
run: cargo sbom --output-format spdx_json_2_3 > sbom.spdx.json
- name: Upload SBOM as release asset
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: sbom.spdx.json
tag: ${{ github.ref }}
overwrite: true
- name: Upload SBOM as workflow artifact
uses: actions/upload-artifact@v4
with:
name: sbom
path: sbom.spdx.json
build:
name: ${{ matrix.target }}
# Wait for the SBOM: the "Download SBOM" step below pulls the `sbom`
# artifact, and attestation needs it. Without this the jobs race and
# build can hit Download SBOM before sbom uploads (or after it fails),
# dying before it uploads the binaries.
needs: sbom
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
# Static musl build — portable across Linux distros (no glibc
# version coupling). Works because TLS is rustls, not OpenSSL.
- os: ubuntu-latest
target: x86_64-unknown-linux-musl
# Cross-compile the Intel target on the Apple-Silicon runner.
# `macos-13` (Intel) runners are scarce and frequently sit
# queued until the 24h timeout; `macos-latest` is plentiful and
# ships both-arch SDKs. Build the no-plugin set (same as
# Windows): `evil_janet`'s C lib only compiles for the host
# arch, so the Janet `plugin` feature can't link for x86_64 when
# cross-building. The native arm64 build below keeps the plugin.
- os: macos-latest
target: x86_64-apple-darwin
cargo_flags: "--no-default-features --features windows-default"
- os: macos-latest
target: aarch64-apple-darwin
# Windows MSVC can't build the Janet `plugin` feature
# (evil_janet needs _setjmp; janetrs 0.8 passes i64 to the
# Windows fseek). Build the default set minus plugin.
- os: windows-latest
target: x86_64-pc-windows-msvc
cargo_flags: "--no-default-features --features windows-default"
steps:
- uses: actions/checkout@v7
- name: Install Rust
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
target: ${{ matrix.target }}
# musl needs the cross linker/toolchain.
- name: Install musl tools
if: matrix.target == 'x86_64-unknown-linux-musl'
run: sudo apt-get update && sudo apt-get install -y musl-tools
- name: Build
run: cargo build --release --target ${{ matrix.target }} ${{ matrix.cargo_flags }}
# Package the `dirge` binary (named via [[bin]] regardless of the
# `dirge-agent` crate name). Windows → .zip, Unix → .tar.gz; emit a
# sha256 alongside so users can verify the download.
- name: Package (Unix)
if: matrix.os != 'windows-latest'
run: |
archive=dirge-${{ matrix.target }}.tar.gz
tar czf "$archive" -C "target/${{ matrix.target }}/release" dirge
shasum -a 256 "$archive" > "$archive.sha256"
- name: Package (Windows)
if: matrix.os == 'windows-latest'
shell: pwsh
run: |
$archive = "dirge-${{ matrix.target }}.zip"
Compress-Archive -Path "target/${{ matrix.target }}/release/dirge.exe" -DestinationPath $archive
(Get-FileHash $archive -Algorithm SHA256).Hash | Out-File -Encoding ascii "$archive.sha256"
- name: Download SBOM
uses: actions/download-artifact@v4
with:
name: sbom
- name: Attest binary (Unix)
if: matrix.os != 'windows-latest'
uses: actions/attest@v4
with:
subject-path: dirge-${{ matrix.target }}.tar.gz
sbom-path: sbom.spdx.json
- name: Attest binary (Windows)
if: matrix.os == 'windows-latest'
uses: actions/attest@v4
with:
subject-path: dirge-${{ matrix.target }}.zip
sbom-path: sbom.spdx.json
- name: Upload release assets
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: dirge-${{ matrix.target }}.*
file_glob: true
tag: ${{ github.ref }}
overwrite: true
nix-binary-package:
name: Bump Nix binary package
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
ref: main
# Full history so the post-build commit can rebase+push to main.
fetch-depth: 0
- name: Install Nix
uses: DeterminateSystems/nix-installer-action@v22
- name: Prefetch release assets and update nix/bin.nix
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
tag="$GITHUB_REF_NAME"
version="${tag#v}"
assets="$(gh release view "$tag" -R "$GITHUB_REPOSITORY" --json assets --jq '.assets[].name')"
python3 - <<'PY' > /tmp/dirge-nix-assets.tsv
import re
from pathlib import Path
text = Path("nix/bin.nix").read_text()
for match in re.finditer(r'"(?P<system>[^"]+)" = \{\s*triple = "(?P<triple>[^"]+)";', text):
print(f"{match.group('system')}\t{match.group('triple')}")
PY
hashes_json="{}"
while IFS=$'\t' read -r system triple; do
asset="dirge-${triple}.tar.gz"
if ! grep -Fxq "$asset" <<< "$assets"; then
echo "Missing release asset: $asset" >&2
exit 1
fi
url="https://github.com/${GITHUB_REPOSITORY}/releases/download/${tag}/${asset}"
hash="$(nix store prefetch-file --hash-type sha256 --json "$url" | jq -r .hash)"
hashes_json="$(jq --arg system "$system" --arg hash "$hash" '. + {($system): $hash}' <<< "$hashes_json")"
done < /tmp/dirge-nix-assets.tsv
HASHES_JSON="$hashes_json" VERSION="$version" python3 - <<'PY'
import json
import os
import re
from pathlib import Path
path = Path("nix/bin.nix")
text = path.read_text()
text = re.sub(r'version = "[^"]+";', f'version = "{os.environ["VERSION"]}";', text, count=1)
hashes = json.loads(os.environ["HASHES_JSON"])
for system, hash_ in hashes.items():
pattern = rf'("{re.escape(system)}" = \{{\n\s+triple = "[^"]+";\n\s+hash = ")[^"]+(";)'
text = re.sub(pattern, rf'\g<1>{hash_}\2', text, count=1)
path.write_text(text)
PY
# Commit the bump straight to `main` instead of opening a PR. The
# PR route (peter-evans/create-pull-request) failed on every release
# with "GitHub Actions is not permitted to create or approve pull
# requests" — a repo Actions toggle that's off. `main` is unprotected
# and this job has contents: write (explicit permissions), so a
# direct push works and needs no extra setting or PAT. A GITHUB_TOKEN
# push doesn't re-trigger workflows, so this can't loop.
- name: Commit nix/bin.nix bump
run: |
set -euo pipefail
if git diff --quiet -- nix/bin.nix; then
echo "nix/bin.nix already current for ${GITHUB_REF_NAME} — nothing to commit"
exit 0
fi
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add nix/bin.nix
git commit -m "Bump Nix dirge-bin to ${GITHUB_REF_NAME}"
# The release job can run minutes after the tag; rebase on any
# main movement so the push fast-forwards.
git pull --rebase origin main
git push origin HEAD:main
# Bump the Homebrew tap (dirge-code/homebrew-dirge) to the just-released
# version + checksums. Runs after every target has uploaded its assets, so
# the .sha256 files exist to read back. Needs a HOMEBREW_TAP_TOKEN secret —
# a PAT (or fine-grained token) with contents:write on the tap repo, since
# the default GITHUB_TOKEN can't push to another repository. If the secret
# is absent the checkout step fails loudly but the binary release above is
# already complete and unaffected.
homebrew:
name: Bump Homebrew formula
needs: build
runs-on: ubuntu-latest
steps:
- name: Resolve version + checksums from the release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
tag="$GITHUB_REF_NAME"
mkdir -p shas
gh release download "$tag" -R "$GITHUB_REPOSITORY" -p '*.tar.gz.sha256' -D shas
{
echo "VERSION=${tag#v}"
echo "ARM_MAC=$(awk '{print $1}' shas/dirge-aarch64-apple-darwin.tar.gz.sha256)"
echo "X86_MAC=$(awk '{print $1}' shas/dirge-x86_64-apple-darwin.tar.gz.sha256)"
echo "X86_LINUX=$(awk '{print $1}' shas/dirge-x86_64-unknown-linux-gnu.tar.gz.sha256)"
} >> "$GITHUB_ENV"
- name: Check out the tap
uses: actions/checkout@v7
with:
repository: dirge-code/homebrew-dirge
token: ${{ secrets.HOMEBREW_TAP_TOKEN }}
path: tap
- name: Update the formula in place
run: |
f=tap/Formula/dirge.rb
# version line + the version segment in every release URL
sed -i \
-e "s|version \"[^\"]*\"|version \"$VERSION\"|" \
-e "s|/releases/download/v[0-9][^/]*/|/releases/download/v$VERSION/|g" \
"$f"
# rewrite each sha256 by the URL that immediately precedes it
awk -v arm="$ARM_MAC" -v xmac="$X86_MAC" -v xlin="$X86_LINUX" '
/aarch64-apple-darwin\.tar\.gz"/ { want="arm" }
/x86_64-apple-darwin\.tar\.gz"/ { want="xmac" }
/x86_64-unknown-linux-gnu\.tar\.gz"/ { want="xlin" }
/sha256 "/ {
v = (want=="arm" ? arm : (want=="xmac" ? xmac : xlin))
sub(/sha256 "[^"]*"/, "sha256 \"" v "\"")
want=""
}
{ print }
' "$f" > "$f.tmp" && mv "$f.tmp" "$f"
- name: Commit and push
run: |
cd tap
if git diff --quiet; then
echo "Formula already at $VERSION — nothing to do."
exit 0
fi
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git commit -am "dirge $VERSION"
git push
# Publish to crates.io (dirge-agent). Was manual, and 0.20.0 never reached
# the registry as a result — `cargo install dirge-agent` went 0.19.29 →
# 0.21.0 (dirge-a9pc). Needs a CARGO_REGISTRY_TOKEN repo secret scoped to
# publish-update on the crate.
#
# `needs: build` because publishing is PERMANENT — a version can be yanked
# but never replaced. Gating on all five targets building means a tag that
# doesn't compile everywhere doesn't become an immovable registry entry.
crates-io:
name: Publish to crates.io
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Install Rust
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: stable
# A tag whose version doesn't match the manifest would publish the
# WRONG version under the right tag, which no later check can undo.
- name: Check the tag matches Cargo.toml
run: |
set -euo pipefail
tag="${GITHUB_REF_NAME#v}"
manifest="$(cargo metadata --no-deps --format-version 1 | jq -r '.packages[0].version')"
if [ "$tag" != "$manifest" ]; then
echo "::error::tag ${GITHUB_REF_NAME} disagrees with Cargo.toml version ${manifest}" >&2
exit 1
fi
echo "tag and manifest agree on ${manifest}"
- name: Publish
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
run: |
set -euo pipefail
if [ -z "${CARGO_REGISTRY_TOKEN:-}" ]; then
echo "::error::CARGO_REGISTRY_TOKEN is not set — add it as a repo secret" >&2
exit 1
fi
# Idempotent: re-running a completed release must not fail. cargo
# refuses to overwrite an existing version, and that refusal is the
# success case here.
set -o pipefail
if cargo publish --locked 2>&1 | tee /tmp/publish.log; then
exit 0
fi
if grep -qE "already (uploaded|exists)|crate version .* is already" /tmp/publish.log; then
echo "dirge-agent ${GITHUB_REF_NAME#v} is already on crates.io — nothing to do"
exit 0
fi
exit 1
# Bump the version shown next to the logo on https://dirge-code.github.io/.
# Also manual until now, and also missed for 0.20.0 — the site still read
# v0.19.29 when 0.21.0 was cut (dirge-a9pc). Same shape as the homebrew job:
# needs a SITE_REPO_TOKEN secret (PAT with contents:write on the site repo)
# because GITHUB_TOKEN can't push to another repository.
site:
name: Bump site version
needs: build
runs-on: ubuntu-latest
steps:
- name: Check out the site
uses: actions/checkout@v7
with:
repository: dirge-code/dirge-code.github.io
token: ${{ secrets.SITE_REPO_TOKEN }}
path: site
- name: Rewrite the brand-ver span
run: |
set -euo pipefail
version="${GITHUB_REF_NAME#v}"
f=site/index.html
sed -i "s|\(class=\"brand-ver\">\)v[0-9][^<]*|\1v${version}|" "$f"
# An unmatched sed is a silent no-op, which is exactly the failure
# mode this job exists to remove. Prove the string is there now.
grep -q "class=\"brand-ver\">v${version}<" "$f" || {
echo "::error::brand-ver span did not end up at v${version} — has the markup changed?" >&2
exit 1
}
- name: Commit and push
run: |
set -euo pipefail
cd site
if git diff --quiet; then
echo "Site already at ${GITHUB_REF_NAME} — nothing to do."
exit 0
fi
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git commit -am "Bump version to ${GITHUB_REF_NAME}"
git push
# The actual fix for dirge-a9pc. Every job above can report success while
# shipping nothing: the homebrew and site seds no-op silently if the markup
# moves, and a skipped job is green by default. This reads each channel back
# from its published source and fails if any of them disagrees with the tag,
# so "a channel didn't ship" is a red X instead of something noticed two
# releases later.
verify-channels:
name: Verify all release channels
needs: [build, nix-binary-package, homebrew, crates-io, site]
runs-on: ubuntu-latest
steps:
- name: Check every channel carries this version
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -uo pipefail
tag="$GITHUB_REF_NAME"
version="${tag#v}"
failed=0
fail() { echo "::error::$1"; failed=1; }
ok() { echo "ok: $1"; }
# Fetch to a file, THEN match. `gh api … | grep -q` looks equivalent
# and isn't: grep -q exits on the first match, gh takes SIGPIPE, and
# pipefail reports the pipeline as failed — a missing-channel error
# for a channel that is fine. It only shows up on inputs long enough
# to still be streaming (index.html), which is the worst way to find
# out.
fetch() { gh api "$1" -H "Accept: application/vnd.github.raw" > "$2"; }
# 1. GitHub release — every packaged target plus its checksum.
assets="$(gh release view "$tag" -R "$GITHUB_REPOSITORY" --json assets --jq '.assets[].name')"
for want in \
dirge-x86_64-unknown-linux-gnu.tar.gz \
dirge-x86_64-unknown-linux-musl.tar.gz \
dirge-x86_64-apple-darwin.tar.gz \
dirge-aarch64-apple-darwin.tar.gz \
dirge-x86_64-pc-windows-msvc.zip \
sbom.spdx.json
do
if grep -Fxq "$want" <<< "$assets"; then ok "release asset $want"
else fail "release $tag is missing asset $want"; fi
case "$want" in
sbom.spdx.json) ;;
*) if grep -Fxq "$want.sha256" <<< "$assets"; then ok "release asset $want.sha256"
else fail "release $tag is missing checksum $want.sha256"; fi ;;
esac
done
# 2. crates.io. The sparse index is CDN-cached, so give it a few
# minutes before calling it missing.
on_registry() {
curl -sf https://index.crates.io/di/rg/dirge-agent > /tmp/crates-index.json || return 1
[ "$(jq -sr --arg v "$version" 'any(.[]; .vers == $v)' /tmp/crates-index.json)" = true ]
}
for attempt in $(seq 1 10); do
on_registry && break
echo "crates.io index has no ${version} yet (attempt ${attempt}/10)"
[ "$attempt" -lt 10 ] && sleep 30
done
if on_registry
then ok "crates.io has dirge-agent ${version}"
else fail "crates.io has no dirge-agent ${version}"; fi
# 3. nix/bin.nix, committed back to main by the bump job.
fetch "repos/$GITHUB_REPOSITORY/contents/nix/bin.nix?ref=main" /tmp/bin.nix
if grep -q "version = \"${version}\";" /tmp/bin.nix
then ok "nix/bin.nix on main is at ${version}"
else fail "nix/bin.nix on main is not at ${version}"; fi
# 4. Homebrew tap.
fetch repos/dirge-code/homebrew-dirge/contents/Formula/dirge.rb /tmp/dirge.rb
if grep -q "version \"${version}\"" /tmp/dirge.rb
then ok "homebrew formula is at ${version}"
else fail "homebrew formula is not at ${version}"; fi
# 5. Site.
fetch repos/dirge-code/dirge-code.github.io/contents/index.html /tmp/index.html
if grep -q "class=\"brand-ver\">v${version}<" /tmp/index.html
then ok "site brand-ver is at v${version}"
else fail "site brand-ver is not at v${version}"; fi
if [ "$failed" -ne 0 ]; then
echo "::error::one or more release channels did not receive ${tag}"
exit 1
fi
echo "all six channels carry ${tag}"