Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
240 changes: 240 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ jobs:
if: ${{ github.ref == 'refs/heads/main' }}
runs-on: ubuntu-latest
environment: Production
# Consumed by `native-bundles` and `github-release`: both check out the tag
# this job created, and the archive names embed the version it computed.
outputs:
next_version: ${{ steps.version.outputs.next_version }}
tag: ${{ steps.version.outputs.tag }}
env:
CRATE_NAME: tinychannels
steps:
Expand Down Expand Up @@ -198,3 +203,238 @@ jobs:
exit 1
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}

# The crate publish above is not the deliverable for `tinychannels-module`:
# it is `publish = false`, and what a host loads is the compiled `cdylib`.
# OpenHuman's `modules::registry` pins each artifact by a SHA-256 digest taken
# verbatim from a release, so without these jobs the module cannot be pinned
# and therefore cannot be loaded at all.
#
# The matrix is per-distro on purpose, not per-target. A `.so` built against
# glibc 2.39 fails to `dlopen` on a 2.35 host with a symbol-version error, so
# the host probes glibc and picks the newest build that could work — which
# only helps if the older build exists.
native-bundles:
name: Module bundle (${{ matrix.id }})
needs: publish-rust
strategy:
# One unavailable runner should not cost the other ten artifacts; the
# asset-count check in `github-release` is what refuses a partial set.
fail-fast: false
matrix:
include:
- id: ubuntu-22.04-x86_64
os: ubuntu-22.04
target: x86_64-unknown-linux-gnu
- id: ubuntu-22.04-arm64
os: ubuntu-22.04-arm
target: aarch64-unknown-linux-gnu
- id: ubuntu-24.04-x86_64
os: ubuntu-24.04
target: x86_64-unknown-linux-gnu
- id: ubuntu-24.04-arm64
os: ubuntu-24.04-arm
target: aarch64-unknown-linux-gnu
- id: macos-15-x86_64
os: macos-15-intel
target: x86_64-apple-darwin
- id: macos-15-arm64
os: macos-15
target: aarch64-apple-darwin
- id: macos-26-x86_64
os: macos-26-intel
target: x86_64-apple-darwin
- id: macos-26-arm64
os: macos-26
target: aarch64-apple-darwin
- id: windows-2022-x86_64
os: windows-2022
target: x86_64-pc-windows-msvc
- id: windows-2025-x86_64
os: windows-2025
target: x86_64-pc-windows-msvc
- id: windows-11-arm64
os: windows-11-arm
target: aarch64-pc-windows-msvc
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v7
with:
# The tag the publish job just created, so the artifact matches the
# released source rather than whatever `main` has moved on to.
ref: ${{ needs.publish-rust.outputs.tag }}
persist-credentials: false
submodules: true

- uses: dtolnay/rust-toolchain@stable
Comment thread
senamakel marked this conversation as resolved.

- uses: Swatinem/rust-cache@v2
Comment thread
senamakel marked this conversation as resolved.

# The whole point of the per-distro matrix is that each `id` describes the
# machine the artifact was built on. A runner image that silently changed
# architecture would produce a correctly-named artifact that cannot load.
- name: Verify native Rust target
shell: bash
env:
EXPECTED_TARGET: ${{ matrix.target }}
run: |
set -euo pipefail
actual_target="$(rustc -vV | sed -n 's/^host: //p')"
[[ "$actual_target" == "$EXPECTED_TARGET" ]]

- name: Build installable module
run: cargo build --locked --release --package tinychannels-module

- name: Assemble Unix module package
if: ${{ runner.os != 'Windows' }}
id: unix_package
shell: bash
env:
BUNDLE_ID: ${{ matrix.id }}
VERSION: ${{ needs.publish-rust.outputs.next_version }}
run: |
set -euo pipefail

library_name="tinychannels_module"
case "$RUNNER_OS" in
Linux) module="target/release/lib${library_name}.so" ;;
macOS) module="target/release/lib${library_name}.dylib" ;;
*) echo "unsupported Unix runner: ${RUNNER_OS}" >&2; exit 1 ;;
esac
package_name="tinychannels-module-${VERSION}-${BUNDLE_ID}"
package_root="dist/${package_name}"
mkdir -p "$package_root"
install -m 755 "$module" "$package_root/"
install -m 644 LICENSE README.md "$package_root/"
# The in-archive digest is the module's own; the release-level
# `checksum.toml` covers the archives. The host checks both.
module_name="$(basename "$module")"
# macOS runners don't ship GNU coreutils' `sha256sum`; only `shasum`
# is guaranteed. Prefer `sha256sum` where it exists (Linux) and fall
# back to `shasum -a 256` (macOS) rather than assuming either.
if command -v sha256sum >/dev/null 2>&1; then
module_hash="$(sha256sum "$package_root/$module_name" | awk '{print $1}')"
else
module_hash="$(shasum -a 256 "$package_root/$module_name" | awk '{print $1}')"
fi
printf '"%s" = "%s"\n' "$module_name" "$module_hash" \
> "$package_root/modules.toml"
tar -C "$package_root" -czf "dist/${package_name}.tar.gz" .
echo "archive=dist/${package_name}.tar.gz" >> "$GITHUB_OUTPUT"

- name: Assemble Windows module package
if: ${{ runner.os == 'Windows' }}
id: windows_package
shell: pwsh
env:
BUNDLE_ID: ${{ matrix.id }}
VERSION: ${{ needs.publish-rust.outputs.next_version }}
run: |
$ErrorActionPreference = 'Stop'
$libraryName = 'tinychannels_module'
$module = "target/release/$libraryName.dll"
$packageName = "tinychannels-module-$env:VERSION-$env:BUNDLE_ID"
$packageRoot = "dist/$packageName"
New-Item -ItemType Directory -Force $packageRoot | Out-Null
Copy-Item -LiteralPath $module, 'LICENSE', 'README.md' -Destination $packageRoot
$hash = (Get-FileHash -LiteralPath $module -Algorithm SHA256).Hash.ToLowerInvariant()
$moduleName = Split-Path -Leaf $module
"`"$moduleName`" = `"$hash`"`n" |
Set-Content -Path "$packageRoot/modules.toml" -Encoding utf8NoBOM
Compress-Archive -Path "$packageRoot/*" -DestinationPath "dist/$packageName.zip"
"archive=dist/$packageName.zip" >> $env:GITHUB_OUTPUT

- name: Upload Unix module package
if: ${{ runner.os != 'Windows' }}
uses: actions/upload-artifact@v7
with:
name: tinychannels-module-${{ matrix.id }}
path: ${{ steps.unix_package.outputs.archive }}
if-no-files-found: error

- name: Upload Windows module package
if: ${{ runner.os == 'Windows' }}
uses: actions/upload-artifact@v7
with:
name: tinychannels-module-${{ matrix.id }}
path: ${{ steps.windows_package.outputs.archive }}
if-no-files-found: error

github-release:
name: Create GitHub release
needs:
- publish-rust
- native-bundles
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
ref: ${{ needs.publish-rust.outputs.tag }}
persist-credentials: false
submodules: true

- name: Download workflow artifacts
uses: actions/download-artifact@v8
with:
pattern: tinychannels-module-*
path: release-assets
merge-multiple: true

- uses: dtolnay/rust-toolchain@stable
Comment thread
senamakel marked this conversation as resolved.

# TinyBus writes the manifest the host compares against, so the format
# cannot drift from what the loader parses. `fail-fast: false` above means
# a missing runner yields a partial set — refuse it rather than publish a
# release some platforms silently cannot install from.
- name: Create release checksum manifest with TinyBus
shell: bash
run: |
set -euo pipefail
mapfile -t assets < <(
find release-assets -type f \
\( -name '*.tar.gz' -o -name '*.zip' \) \
| sort
)
if [[ ${#assets[@]} -ne 11 ]]; then
printf 'expected 11 module archives, found %s:\n' "${#assets[@]}" >&2
find release-assets -type f -print >&2 || true
exit 1
fi
checksum_args=()
for asset in "${assets[@]}"; do checksum_args+=(--path "$asset"); done
cargo run --manifest-path vendor/tinybus/Cargo.toml --locked \
--package tinybus --all-features --bin tinybus -- \
modules checksum "${checksum_args[@]}" --output release-assets/checksum.toml

- name: Create release and upload assets
env:
GH_TOKEN: ${{ github.token }}
RELEASE_TAG: ${{ needs.publish-rust.outputs.tag }}
REPOSITORY: ${{ github.repository }}
run: |
set -euo pipefail
mapfile -t release_files < <(find release-assets -type f | sort)
gh release create "$RELEASE_TAG" "${release_files[@]}" \
--repo "$REPOSITORY" \
--verify-tag \
--title "$RELEASE_TAG" \
--generate-notes

# Downloads what was just published and loads it the way a host does, so a
# release that cannot actually be installed fails here rather than in the
# field on whichever platform nobody tested.
- name: Verify the published module through TinyBus
shell: bash
env:
RELEASE_TAG: ${{ needs.publish-rust.outputs.tag }}
REPOSITORY: ${{ github.repository }}
VERSION: ${{ needs.publish-rust.outputs.next_version }}
run: |
set -euo pipefail
archive="tinychannels-module-${VERSION}-ubuntu-24.04-x86_64.tar.gz"
release_url="https://github.com/${REPOSITORY}/releases/tag/${RELEASE_TAG}"
sha256="$(sed -n "s/^\"${archive}\" = \"\([0-9a-f]\{64\}\)\"$/\1/p" release-assets/checksum.toml)"
test -n "$sha256"
cargo run --manifest-path vendor/tinybus/Cargo.toml --locked \
--package tinybus --all-features --example github_module_host -- \
"$release_url" "$archive" "$sha256"
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@
target/
**/*.rs.bk

# Release staging built by the `native-bundles` job. Local runs of that
# packaging step leave a multi-megabyte `cdylib` and its archive here, and an
# auto-commit hook will happily commit them.
dist/

# Local secrets — never commit; copy .env.example to .env
.env
.DS_Store