feat: provide way to build and attach platforms
Pipeline / check (push) Successful in 1m56s
Pipeline / release (push) Skipped
Pipeline / docs (push) Successful in 1m47s
Pipeline / nightly (push) Successful in 6m41s

This commit is contained in:
2026-08-08 11:46:15 -04:00
parent fdc53ae210
commit 3a7a0d5873
12 changed files with 230 additions and 81 deletions
+2
View File
@@ -0,0 +1,2 @@
GITEA_API =
GITEA_TOKEN =
+32 -32
View File
@@ -27,6 +27,35 @@ jobs:
- name: Verify (formatting, lint, tests, docs)
run: pixi run check
docs:
needs: check
if: ${{ (gitea.event_name == 'push' && (gitea.ref == 'refs/heads/main' || startsWith(gitea.ref, 'refs/tags/'))) || gitea.event_name == 'workflow_dispatch' }}
runs-on: pixi-build-rust
steps:
- uses: actions/checkout@v4
- name: Build the API docs
env:
RUSTDOCFLAGS: "--html-before-content ${{ gitea.workspace }}/scripts/docs-banner.html"
run: |
pixi install --locked
pixi run doc-build
- name: Redirect the channel root to the crate page
run: |
printf '%s\n' "<!doctype html><meta http-equiv=\"refresh\" content=\"0; url=${CRATE}/\">" > target/doc/index.html
- name: Publish the channel to /srv/www
run: |
case "${{ gitea.ref }}" in
refs/tags/*) channel=release ;;
*) channel=nightly ;;
esac
bash scripts/publish-docs.sh "$channel"
- name: Point the site root at the latest release, else nightly
run: bash scripts/redirect-docs.sh
nightly:
needs: check
if: ${{ (gitea.event_name == 'push' && gitea.ref == 'refs/heads/main') || gitea.event_name == 'schedule' || gitea.event_name == 'workflow_dispatch' }}
@@ -42,7 +71,7 @@ jobs:
- name: Install the pinned toolchain
run: pixi install
- name: Build the release binary
- name: Build and stage the bundle
run: |
COURSEBANK_VERSION="$(bash scripts/nightly-version.sh)"
export COURSEBANK_VERSION
@@ -94,7 +123,7 @@ jobs:
pixi run dist
- name: Package
run: bash scripts/package.sh "coursebank-$(bash scripts/version.sh)-linux-x64.tar.gz" dist
run: bash scripts/package.sh "coursebank-$(bash scripts/version.sh)-$(bash scripts/platform-tag.sh).tar.gz" dist
- name: Publish the release
env:
@@ -104,35 +133,6 @@ jobs:
TAG: ${{ gitea.ref_name }}
BODY: See CHANGELOG.md.
run: |
asset="coursebank-$(bash scripts/version.sh)-linux-x64.tar.gz"
asset="coursebank-$(bash scripts/version.sh)-$(bash scripts/platform-tag.sh).tar.gz"
export ASSETS="$asset $asset.sha256"
bash scripts/gitea-release.sh
docs:
needs: check
if: ${{ (gitea.event_name == 'push' && (gitea.ref == 'refs/heads/main' || startsWith(gitea.ref, 'refs/tags/'))) || gitea.event_name == 'workflow_dispatch' }}
runs-on: pixi-build-rust
steps:
- uses: actions/checkout@v4
- name: Build the API docs
env:
RUSTDOCFLAGS: "--html-before-content ${{ gitea.workspace }}/scripts/docs-banner.html"
run: |
pixi install --locked
pixi run doc-build
- name: Redirect the channel root to the crate page
run: |
printf '%s\n' "<!doctype html><meta http-equiv=\"refresh\" content=\"0; url=${CRATE}/\">" > target/doc/index.html
- name: Publish the channel to /srv/www
run: |
case "${{ gitea.ref }}" in
refs/tags/*) channel=release ;;
*) channel=nightly ;;
esac
bash scripts/publish-docs.sh "$channel"
- name: Point the site root at the latest release, else nightly
run: bash scripts/redirect-docs.sh
+2
View File
@@ -3,6 +3,8 @@ preview
/dist/
/THIRD-PARTY-LICENSES.txt
.env
# pixi environments
.pixi/*
!.pixi/config.toml
+76 -45
View File
@@ -5,66 +5,97 @@ authors = ["Scientific Computing Studio <opensource@scient.ing>"]
channels = ["conda-forge"]
platforms = ["linux-64", "osx-arm64", "osx-64"]
[dependencies]
rust = ">=1.96.0,<1.97"
c-compiler = "*"
pkg-config = "*"
[tasks]
# --- Build and run ---
build = { cmd = "cargo build --release", description = "Compile the release binary at target/release/coursebank" }
debug = { cmd = "cargo build", description = "Compile the debug binary" }
build-lean = { cmd = "cargo build --release --no-default-features", description = "Release build without the parquet feature" }
install = { cmd = "cargo install --path . --locked", description = "Install the coursebank binary onto your PATH" }
clean = { cmd = "cargo clean", description = "Remove the target directory" }
cb = { cmd = "cargo run --release --quiet --", description = "Run the CLI, e.g., `pixi run cb validate -C path/to/course`" }
# --- Quality gate ---
format = { cmd = "cargo fmt --all", description = "Reformat the source in place" }
fmt-check = { cmd = "cargo fmt --all --check", description = "Fail if the source is not formatted; does not rewrite" }
lint = { cmd = "cargo clippy --all-targets -- -D warnings", description = "Run clippy with warnings treated as errors" }
tests = { cmd = "cargo test", description = "Run all tests: unit, integration, and doctests" }
doctests = { cmd = "cargo test --doc", description = "Run only the documentation examples" }
check-docs = { cmd = "cargo test --test docs", description = "Check that included markdown fences declare a language" }
check = { depends-on = ["fmt-check", "lint", "tests", "doctests", "check-docs", "doc-build"], description = "Full verify-only gate: formatting, lint, tests, and doc build" }
# --- Documentation ---
doc = { cmd = "cargo doc --no-deps --open", depends-on = ["check-docs"], description = "Build the API docs and open them in a browser" }
doc-build = { cmd = "cargo doc --no-deps", depends-on = ["check-docs"], description = "Build the API docs without opening a browser" }
# --- Release packaging ---
setup-tools = { cmd = "cargo install cargo-about --locked --features cli", description = "Install cargo-about, which the licenses task needs" }
licenses = { cmd = "cargo about generate about.hbs -o THIRD-PARTY-LICENSES.txt", description = "Regenerate THIRD-PARTY-LICENSES.txt" }
dist = { cmd = "mkdir -p dist && cp target/release/coursebank dist/ && cp LICENSE.md THIRD-PARTY-LICENSES.txt dist/", depends-on = ["build", "licenses"], description = "Assemble a release bundle (binary and license notices) in dist/" }
[environments]
dev = ["dev"]
docs = ["docs"]
release = ["release"]
# Developer tooling: editor support, a debugger, a faster test runner, and a
# file watcher.
[dependencies]
rust = ">=1.96.0,<1.97"
c-compiler = "*"
pkg-config = "*"
# --- Build and run ---
[tasks.build]
description = "Compile the release binary at target/release/coursebank"
cmd = "cargo build --release"
[tasks.debug]
description = "Compile the debug binary"
cmd = "cargo build"
[tasks.build-lean]
description = "Release build without the parquet feature"
cmd = "cargo build --release --no-default-features"
[tasks.install]
description = "Install the coursebank binary onto your PATH"
cmd = "cargo install --path . --locked"
[tasks.clean]
description = "Remove the target directory"
cmd = "cargo clean"
[tasks.cb]
description = "Run the CLI, e.g., `pixi run cb validate -C path/to/course`"
cmd = "cargo run --release --quiet --"
# --- Quality gate ---
[tasks.format]
description = "Reformat the source in place"
cmd = "cargo fmt --all"
[tasks.fmt-check]
description = "Fail if the source is not formatted; does not rewrite"
cmd = "cargo fmt --all --check"
[tasks.lint]
description = "Run clippy with warnings treated as errors"
cmd = "cargo clippy --all-targets -- -D warnings"
[tasks.tests]
description = "Run all tests: unit, integration, and doctests"
cmd = "cargo test"
[tasks.doctests]
description = "Run only the documentation examples"
cmd = "cargo test --doc"
[tasks.check-docs]
description = "Check that included markdown fences declare a language"
cmd = "cargo test --test docs"
[tasks.check]
description = "Full verify-only gate: formatting, lint, tests, and doc build"
depends-on = ["fmt-check", "lint", "tests", "doctests", "check-docs", "doc-build"]
# --- Documentation ---
[tasks.doc]
description = "Build the API docs and open them in a browser"
depends-on = ["check-docs"]
cmd = "cargo doc --no-deps --open"
[tasks.doc-build]
description = "Build the API docs without opening a browser"
depends-on = ["check-docs"]
cmd = "cargo doc --no-deps"
# --- Release packaging ---
[tasks.setup-tools]
description = "Install cargo-about, which the licenses task needs"
cmd = "cargo install cargo-about --locked --features cli"
[tasks.licenses]
description = "Regenerate THIRD-PARTY-LICENSES.txt"
cmd = "cargo about generate about.hbs -o THIRD-PARTY-LICENSES.txt"
[tasks.dist]
description = "Assemble a release bundle (binary and license notices) in dist/"
cmd = "mkdir -p dist && cp target/release/coursebank dist/ && cp LICENSE.md THIRD-PARTY-LICENSES.txt dist/"
depends-on = ["build", "licenses"]
[feature.dev.dependencies]
rust-analyzer = "*"
lldb = "*"
cargo-nextest = "*"
[feature.dev.tasks]
nextest = { cmd = "cargo nextest run", description = "Run the test suite with nextest" }
[feature.dev.tasks.nextest]
description = "Run the test suite with nextest"
cmd = "cargo nextest run"
# Typst is only needed to typeset exported .typ files by hand. The exporter
# writes .typ on its own and does not require typst to be installed.
[feature.docs.dependencies]
typst = "*"
[feature.docs.tasks]
typeset = { cmd = "typst compile", description = "Compile a .typ file to PDF" }
[feature.docs.tasks.typeset]
description = "Compile a .typ file to PDF"
cmd = "typst compile"
# Release tooling, kept in its own environment so the default and dev
# environments do not pull in Python. bump-my-version reads .bumpversion.toml,
# which encodes the YY.MM.PATCH scheme.
[feature.release.dependencies]
bump-my-version = "*"
+33
View File
@@ -0,0 +1,33 @@
#!/usr/bin/env bash
# Attach files to an existing Gitea release, found by tag. Re-runnable: an
# asset of the same name is replaced. Use this to add a build CI can't produce
# (e.g., the macOS arm64 bundle, built on a Mac) to a release CI already made.
#
# Required env:
# GITEA_API e.g., https://git.scient.ing/api/v1/repos/<owner>/<repo>
# GITEA_TOKEN a token with contents:write
# TAG the release tag the assets belong to
# ASSETS space-separated list of files to upload
# Optional env:
# JQ jq command (default: jq); on a Mac without jq, set to
# "pixi exec --spec jq -- jq"
set -euo pipefail
: "${GITEA_API:?}"; : "${GITEA_TOKEN:?}"; : "${TAG:?}"; : "${ASSETS:?}"
: "${JQ:=jq}"
auth="Authorization: token ${GITEA_TOKEN}"
rid="$(curl -fsSL -H "$auth" "${GITEA_API}/releases/tags/${TAG}" | $JQ -r '.id')"
[ -n "$rid" ] && [ "$rid" != "null" ] || { echo "no release found for tag ${TAG}" >&2; exit 1; }
for f in $ASSETS; do
n="$(basename "$f")"
for aid in $(curl -fsSL -H "$auth" "${GITEA_API}/releases/${rid}/assets" \
| $JQ -r --arg n "$n" '.[] | select(.name==$n) | .id'); do
curl -fsSL -X DELETE -H "$auth" "${GITEA_API}/releases/${rid}/assets/${aid}" || true
done
echo "uploading $n to release ${TAG}"
curl -fsSL -X POST -H "$auth" -F "attachment=@${f}" \
"${GITEA_API}/releases/${rid}/assets?name=${n}"
done
+18
View File
@@ -0,0 +1,18 @@
#!/usr/bin/env bash
# Fail unless the release tag matches the crate version.
# Env: TAG (a leading "v" is tolerated). Run from the repository root.
set -euo pipefail
: "${TAG:?set TAG to the tag being released}"
here="$(cd "$(dirname "$0")" && pwd)"
tag="${TAG#v}"
version="$(bash "$here/version.sh")"
if [ "$tag" != "$version" ]; then
echo "tag ${TAG} does not match Cargo.toml version ${version}" >&2
exit 1
fi
echo "tag ${TAG} matches crate version ${version}"
+27
View File
@@ -0,0 +1,27 @@
#!/usr/bin/env bash
# Build the host-native release bundle and attach it to an existing Gitea
# release. Meant for targets CI cannot build. Check out
# the tagged commit first, so the crate version matches the tag.
#
# Required env: GITEA_API, GITEA_TOKEN (contents:write).
# Optional env: TAG (defaults to the crate version), JQ.
set -euo pipefail
here="$(cd "$(dirname "$0")" && pwd)"
: "${GITEA_API:?set GITEA_API, e.g., https://git.scient.ing/api/v1/repos/<owner>/<repo>}"
: "${GITEA_TOKEN:?set GITEA_TOKEN to a token with contents:write}"
version="$(bash "$here/version.sh")"
tag="${TAG:-$version}"
asset="coursebank-${version}-$(bash "$here/platform-tag.sh").tar.gz"
echo "building ${asset} for release ${tag}"
pixi run setup-tools
pixi run dist
bash "$here/package.sh" "$asset" dist
TAG="$tag" ASSETS="$asset $asset.sha256" bash "$here/attach-asset.sh"
echo "done: ${asset} attached to ${tag}"
+6 -1
View File
@@ -2,7 +2,7 @@
# Create (or replace) a Gitea release and upload assets, using the Gitea API.
#
# Required env:
# GITEA_API e.g. https://git.scient.ing/api/v1/repos/<owner>/<repo>
# GITEA_API e.g., https://git.scient.ing/api/v1/repos/<owner>/<repo>
# GITEA_TOKEN a token with contents:write
# TAG the release tag
# ASSETS space-separated list of files to attach
@@ -14,7 +14,9 @@
# TARGET_COMMITISH (default: empty; Gitea uses the default branch)
# JQ jq command (default: jq); set to e.g. "pixi exec --spec jq -- jq"
# DRY_RUN set to anything to print actions instead of calling the API
set -euo pipefail
: "${GITEA_API:?}"; : "${GITEA_TOKEN:?}"; : "${TAG:?}"; : "${ASSETS:?}"
: "${JQ:=jq}"
name="${RELEASE_NAME:-$TAG}"
@@ -24,6 +26,7 @@ replace="${REPLACE:-false}"
target="${TARGET_COMMITISH:-}"
auth="Authorization: token ${GITEA_TOKEN}"
dry="${DRY_RUN:-}"
say() { echo "[gitea-release] $*"; }
if [ "$replace" = "true" ]; then
@@ -66,4 +69,6 @@ for f in $ASSETS; do
"${GITEA_API}/releases/${rid}/assets?name=${n}"
fi
done
say "done"
+5 -3
View File
@@ -1,4 +1,5 @@
#!/usr/bin/env bash
# Print an auto-generated nightly version string derived from git history:
#
# <last-release-tag>+<commits-since>.g<short-sha> e.g., 26.8.0+14.gb1a2c3d4
@@ -12,14 +13,15 @@
# and never sorts ahead of the release it is based on. Needs full history and
# tags (check out with fetch-depth: 0 and fetch-tags: true). Release tags are
# matched as N.N.N; the moving `nightly` tag is ignored.
set -euo pipefail
sha="$(git rev-parse --short=8 HEAD)"
if desc="$(git describe --tags --long \
--match '[0-9]*.[0-9]*.[0-9]*' --exclude nightly 2>/dev/null)"; then
# desc is <tag>-<n>-g<hash>; the tag itself has no '-', so strip from the right.
rest="${desc%-g*}" # <tag>-<n>
n="${rest##*-}" # <n>
tag="${rest%-*}" # <tag>
rest="${desc%-g*}"
n="${rest##*-}"
tag="${rest%-*}"
echo "${tag}+${n}.g${sha}"
else
n="$(git rev-list --count HEAD)"
+4
View File
@@ -1,9 +1,13 @@
#!/usr/bin/env bash
# Tar the contents of a directory into an asset and write its checksum.
# Usage: package.sh <asset.tar.gz> <dir>
set -euo pipefail
asset="${1:?usage: package.sh <asset.tar.gz> <dir>}"
dir="${2:?usage: package.sh <asset.tar.gz> <dir>}"
tar -czf "$asset" -C "$dir" .
sha256sum "$asset" > "$asset.sha256"
echo "packaged $asset ($(wc -c < "$asset") bytes) and $asset.sha256"
+20
View File
@@ -0,0 +1,20 @@
#!/usr/bin/env bash
# Print a short platform tag for asset names, derived from the host:
# linux-x64, linux-arm64, macos-arm64, macos-x64, ...
set -euo pipefail
case "$(uname -s)" in
Linux) os=linux ;;
Darwin) os=macos ;;
*) os="$(uname -s | tr '[:upper:]' '[:lower:]')" ;;
esac
case "$(uname -m)" in
x86_64|amd64) arch=x64 ;;
aarch64|arm64) arch=arm64 ;;
*) arch="$(uname -m)" ;;
esac
echo "${os}-${arch}"
+5
View File
@@ -3,13 +3,18 @@
# rustdoc serves the crate under <crate>/, so the target includes that segment.
# Env: WWW_ROOT (default /srv/www), SITE_SLUG (default coursebank),
# CRATE (default coursebank).
set -euo pipefail
root="${WWW_ROOT:-/srv/www}/${SITE_SLUG:-coursebank}"
crate="${CRATE:-coursebank}"
if [ -e "$root/release/$crate/index.html" ]; then
target="release/$crate/"
else
target="nightly/$crate/"
fi
printf '%s\n' "<!doctype html><meta http-equiv=\"refresh\" content=\"0; url=${target}\">" > "$root/index.html"
echo "site root -> $target"