wai lock (project.lock) writes wai.lock beside wai.json: wai's version and the SHA-256 of its catalogue, the registry's, the template's files as this binary holds them, each parameter set's pin (SPEC 3.1), the packaging policy's canonical digest by wai-pack policy, and each conformance corpus's digest (conformance-results 2.1). The file is RFC 8785 on one line with no line ending, no time and no path, so the same inputs give the same bytes on any host and a checkout with core.autocrlf=true has nothing to convert. A member with nothing to pin is absent. wai lock --check holds the file to what it pins. The reader refuses a later version as that version, a member it does not know, a member named twice, and bytes that are not the canonical form. wai check (project.check) validates a project without building it and reports every finding: wai.json, each file it states, required capabilities against the registry, the app by wai app check, what a build would make of the site, and the lock member by member. What it could not run here says not checked and is never counted as holding: exit 1 when a check failed, 5 when none failed and one or more could not run. wai build (project.build) runs the same checks, refuses with nothing written when one fails, then writes dist/: the site, the app, and the package by wai-pack pack and verify, made beside dist/ and renamed into place. It ends with dist/wai-build.json, naming wai.json's canonical digest and wai.lock's. Two builds in two directories at two times give the same tree. wai.json gains the members these read (requires.capabilities, priors, package, conform.suites), and its reader names every problem. The three are in the catalogue and in wai mcp's core toolset, with their schemas under wai/schema/. wai/project-conformance/ pins the lock's bytes and holds a second reader in Python, standard library only, that recomputes every digest. The examples, wai/LICENSE and the corpora a suite reads are never converted on a checkout, and a test holds each to that. Studio's actions are regenerated from the catalogue. Co-Authored-By: Claude Opus 5.5 <[email protected]>
288 lines
15 KiB
YAML
288 lines
15 KiB
YAML
name: wai command line release
|
|
|
|
# On a tag wai-v<version> (the version wai/wai-rs/Cargo.toml states), builds the quickstart
|
|
# archive (wai/quickstart/build.sh) on each runner and attaches it to the forge release of that
|
|
# tag, each archive with its SHA-256. An archive holds bin/ (wai with the feature quickstart,
|
|
# wai-relay, wai-room, wai-ingest, wai-remote-host, wai-pack, wai-quantum), share/wai/web/ (the
|
|
# room client) and share/wai/studio/ (WAI Studio, which `wai dev` serves at /_wai/), both built
|
|
# once, on the macOS host, which has the WebAssembly toolchain and Node:
|
|
# macOS aarch64-apple-darwin, and x86_64-apple-darwin on the Intel Mac (macos-x86_64);
|
|
# Linux x86_64- and aarch64-unknown-linux-musl, static, so any distribution runs it;
|
|
# Windows x86_64-pc-windows-msvc, as a zip.
|
|
# The first job checks the tag against the crate's version and the installer's, and that each
|
|
# project file schema (wai/schema/) is served at its $id as committed: every project file the
|
|
# release writes names its schema there, so a tag does not build until the site serves it.
|
|
# Each archive's own `wai` runs before it is attached (create --list, hello, verify, and on
|
|
# the hosts that run it, dev --check), and `wai doctor --json` from the unpacked archive, its bin
|
|
# first on PATH, must pass every install and stack check: this wai, the relay and the room service
|
|
# beside it and of its version, the room client's and Studio's files
|
|
# (wai/quickstart/test/archive_doctor.py). The ports and browsers depend on the runner: they are
|
|
# printed and not required. On macOS this runs where the triple is the host's. A last job writes the Homebrew formula and the Scoop
|
|
# manifest for the archives attached and attaches them too; they go into the tap and the bucket
|
|
# by hand.
|
|
#
|
|
# Uploads go to the forge directly over the private network (FORGE_API), never through the
|
|
# public tunnel. A runner that is down leaves its platform's archive missing: the release lists
|
|
# what each build attached.
|
|
|
|
concurrency:
|
|
group: ${{ github.workflow }}-${{ github.ref }}
|
|
cancel-in-progress: true
|
|
|
|
on:
|
|
push:
|
|
tags: ['wai-v*']
|
|
|
|
permissions:
|
|
contents: write
|
|
|
|
env:
|
|
CARGO_TERM_COLOR: always
|
|
FORGE_API: http://100.64.224.0:3400/api/v1
|
|
FORGE_REPO: Transaction-Science/open-standards
|
|
PUBLIC: https://git.transaction.science/Transaction-Science/open-standards
|
|
|
|
jobs:
|
|
version:
|
|
name: the tag is the crate's version
|
|
runs-on: linux
|
|
outputs:
|
|
version: ${{ steps.v.outputs.version }}
|
|
steps:
|
|
- uses: actions/checkout@v5
|
|
- id: v
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
v="${GITHUB_REF_NAME#wai-v}"
|
|
crate="$(sed -n 's/^version = "\(.*\)"$/\1/p' wai/wai-rs/Cargo.toml | head -1)"
|
|
[ "$v" = "$crate" ] || { echo "::error::the tag says $v, wai/wai-rs/Cargo.toml says $crate"; exit 1; }
|
|
grep -q "WAI_VERSION:-$v}" wai/quickstart/install.sh || { echo "::error::wai/quickstart/install.sh does not install $v"; exit 1; }
|
|
# Every project file, lock and build record this release writes names its schema by URL
|
|
# ($schema), which editors and agents fetch: each committed schema is served at its $id,
|
|
# as committed, before a release writes a file that names it (wai/schema/README.md).
|
|
for f in wai/schema/*.schema.json; do
|
|
python3 - "$f" <<'PY' || exit 1
|
|
import json, sys, urllib.request
|
|
f = sys.argv[1]
|
|
want = json.load(open(f))
|
|
url = want["$id"]
|
|
try:
|
|
with urllib.request.urlopen(urllib.request.Request(url, headers={"User-Agent": "wai-release"}), timeout=30) as r:
|
|
got = json.loads(r.read())
|
|
except Exception as e:
|
|
sys.exit(f"::error::{url} does not serve {f} ({e}): deploy the site with the schema before the release")
|
|
if got != want:
|
|
sys.exit(f"::error::{url} serves a schema other than {f}: deploy the site with the schema before the release")
|
|
print(f"{url} serves {f}")
|
|
PY
|
|
done
|
|
echo "version=$v" >> "$GITHUB_OUTPUT"
|
|
|
|
# The room client and WAI Studio, once: the same files on every platform.
|
|
web:
|
|
name: the room client and WAI Studio
|
|
needs: version
|
|
runs-on: macos
|
|
timeout-minutes: 60
|
|
steps:
|
|
- uses: actions/checkout@v5
|
|
# launchd's PATH omits cargo and node on the macOS host runner.
|
|
- name: Put cargo and node on PATH
|
|
shell: bash
|
|
run: |
|
|
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
|
echo "/opt/homebrew/bin" >> "$GITHUB_PATH"
|
|
- name: Build and attach them
|
|
shell: bash
|
|
env:
|
|
FORGE_TOKEN: ${{ github.token }}
|
|
run: |
|
|
set -euo pipefail
|
|
rustup target add wasm32-unknown-unknown
|
|
command -v wasm-bindgen >/dev/null && wasm-bindgen --version | grep -q ' 0\.2\.127$' || cargo install --locked wasm-bindgen-cli --version 0.2.127
|
|
v="${{ needs.version.outputs.version }}"
|
|
bash wai/quickstart/build-web.sh "$RUNNER_TEMP/web"
|
|
bash wai/quickstart/build-studio.sh "$RUNNER_TEMP/studio"
|
|
mkdir -p "$RUNNER_TEMP/dist"
|
|
tar -C "$RUNNER_TEMP" -czf "$RUNNER_TEMP/dist/wai-web-$v.tar.gz" web studio
|
|
( cd "$RUNNER_TEMP/dist" && shasum -a 256 "wai-web-$v.tar.gz" > "wai-web-$v.tar.gz.sha256" )
|
|
bash wai/quickstart/publish.sh "$GITHUB_REF_NAME" "wai $v" "$RUNNER_TEMP"/dist/*
|
|
|
|
macos:
|
|
name: macOS · ${{ matrix.triple }}
|
|
needs: [version, web]
|
|
# Each archive on a Mac of its own architecture, so its own `wai` runs before it is
|
|
# attached (the checks below run when the triple is the host's): x86_64 on the Intel Mac.
|
|
runs-on: ${{ matrix.runner }}
|
|
timeout-minutes: 120
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
include:
|
|
- { triple: aarch64-apple-darwin, runner: macos }
|
|
- { triple: x86_64-apple-darwin, runner: macos-x86_64 }
|
|
steps:
|
|
- uses: actions/checkout@v5
|
|
- name: Put cargo on PATH
|
|
shell: bash
|
|
run: echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
|
- name: Build, check and package
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
v="${{ needs.version.outputs.version }}"
|
|
rustup target add "${{ matrix.triple }}"
|
|
curl -fsS -o "$RUNNER_TEMP/web.tar.gz" "${FORGE_API%/api/v1}/$FORGE_REPO/releases/download/$GITHUB_REF_NAME/wai-web-$v.tar.gz"
|
|
tar -C "$RUNNER_TEMP" -xzf "$RUNNER_TEMP/web.tar.gz"
|
|
nice -n 10 bash wai/quickstart/build.sh "$RUNNER_TEMP/dist" --web "$RUNNER_TEMP/web" --studio "$RUNNER_TEMP/studio" --target "${{ matrix.triple }}" --version "$v"
|
|
if [ "${{ matrix.triple }}" = "$(rustc -vV | sed -n 's/^host: //p')" ]; then
|
|
t="$RUNNER_TEMP/try"; mkdir -p "$t"; tar -C "$t" -xzf "$RUNNER_TEMP/dist/wai-$v-${{ matrix.triple }}.tar.gz"
|
|
b="$t/wai-$v-${{ matrix.triple }}/bin"
|
|
(cd "$t" && "$b/wai" hello h >/dev/null && "$b/wai" verify h >/dev/null && mkdir p && cd p && "$b/wai" dev --check --port 0)
|
|
python3 -I wai/quickstart/test/archive_doctor.py "$t/wai-$v-${{ matrix.triple }}"
|
|
fi
|
|
- name: Attach to the release
|
|
shell: bash
|
|
env:
|
|
FORGE_TOKEN: ${{ github.token }}
|
|
run: bash wai/quickstart/publish.sh "$GITHUB_REF_NAME" "wai ${{ needs.version.outputs.version }}" "$RUNNER_TEMP"/dist/*
|
|
|
|
linux:
|
|
name: Linux · ${{ matrix.triple }}
|
|
needs: [version, web]
|
|
runs-on: ${{ matrix.runner }}
|
|
timeout-minutes: 120
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
include:
|
|
- runner: linux
|
|
triple: x86_64-unknown-linux-musl
|
|
- runner: linux-arm64
|
|
triple: aarch64-unknown-linux-musl
|
|
steps:
|
|
- uses: actions/checkout@v5
|
|
- name: Install Rust 1.99.1, the musl target and musl-gcc
|
|
shell: bash
|
|
run: |
|
|
export PATH="$HOME/.cargo/bin:$PATH"
|
|
command -v rustup >/dev/null 2>&1 || \
|
|
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain 1.99.1
|
|
rustup toolchain install 1.99.1 --profile minimal
|
|
rustup default 1.99.1
|
|
rustup target add "${{ matrix.triple }}"
|
|
SUDO=""; [ "$(id -u)" -ne 0 ] && SUDO="sudo"
|
|
$SUDO apt-get update -qq
|
|
$SUDO apt-get install -y -qq musl-tools python3 curl
|
|
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
|
- name: Build, check and package
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
v="${{ needs.version.outputs.version }}"
|
|
t="${{ matrix.triple }}"
|
|
T="$(echo "$t" | tr 'a-z-' 'A-Z_')"
|
|
# Static, as the quantum command line is built (wai-quantum-cli-release.yml).
|
|
export "CC_${t//-/_}=musl-gcc" "CARGO_TARGET_${T}_LINKER=musl-gcc"
|
|
export RUSTFLAGS="-C target-feature=+crt-static -C relocation-model=static"
|
|
curl -fsS -o "$RUNNER_TEMP/web.tar.gz" "${FORGE_API%/api/v1}/$FORGE_REPO/releases/download/$GITHUB_REF_NAME/wai-web-$v.tar.gz"
|
|
tar -C "$RUNNER_TEMP" -xzf "$RUNNER_TEMP/web.tar.gz"
|
|
bash wai/quickstart/build.sh "$RUNNER_TEMP/dist" --web "$RUNNER_TEMP/web" --studio "$RUNNER_TEMP/studio" --target "$t" --version "$v"
|
|
d="$RUNNER_TEMP/try"; mkdir -p "$d"; tar -C "$d" -xzf "$RUNNER_TEMP/dist/wai-$v-$t.tar.gz"
|
|
b="$d/wai-$v-$t/bin"
|
|
(cd "$d" && "$b/wai" hello h >/dev/null && "$b/wai" verify h >/dev/null && mkdir p && cd p && "$b/wai" dev --check --port 0)
|
|
python3 -I wai/quickstart/test/archive_doctor.py "$d/wai-$v-$t"
|
|
- name: Attach to the release
|
|
shell: bash
|
|
env:
|
|
FORGE_TOKEN: ${{ github.token }}
|
|
run: bash wai/quickstart/publish.sh "$GITHUB_REF_NAME" "wai ${{ needs.version.outputs.version }}" "$RUNNER_TEMP"/dist/*
|
|
|
|
windows:
|
|
name: Windows · x86_64-pc-windows-msvc
|
|
needs: [version, web]
|
|
runs-on: windows
|
|
timeout-minutes: 120
|
|
env:
|
|
CI: ""
|
|
steps:
|
|
- uses: actions/checkout@v5
|
|
- name: Build, check and package
|
|
shell: pwsh
|
|
run: |
|
|
$ErrorActionPreference = 'Stop'
|
|
Remove-Item Env:CI -ErrorAction SilentlyContinue
|
|
$v = "${{ needs.version.outputs.version }}"
|
|
$t = "x86_64-pc-windows-msvc"
|
|
$web = Join-Path $env:RUNNER_TEMP 'web.tar.gz'
|
|
curl.exe -fsS -o $web "$($env:FORGE_API -replace '/api/v1$','')/$env:FORGE_REPO/releases/download/$env:GITHUB_REF_NAME/wai-web-$v.tar.gz"
|
|
tar -xzf $web -C $env:RUNNER_TEMP
|
|
$name = "wai-$v-$t"
|
|
$stage = Join-Path $env:RUNNER_TEMP $name
|
|
$dist = Join-Path $env:RUNNER_TEMP 'dist'
|
|
Remove-Item $stage, $dist -Recurse -Force -ErrorAction Ignore
|
|
New-Item -ItemType Directory -Force -Path (Join-Path $stage 'bin'), (Join-Path $stage 'share\wai'), $dist | Out-Null
|
|
$builds = @(
|
|
@{ dir = 'wai\wai-rs'; bin = 'wai'; args = @('--no-default-features', '--features', 'quickstart', '--bin', 'wai') },
|
|
@{ dir = 'joule-code\joule-code-rs'; bin = 'wai-relay'; args = @('-p', 'joule-moq-relay', '--bin', 'wai-relay') },
|
|
@{ dir = 'joule-code\joule-code-rs\crates\joule-moq-room'; bin = 'wai-room'; args = @('--bin', 'wai-room') },
|
|
@{ dir = 'joule-code\joule-code-rs'; bin = 'wai-ingest'; args = @('-p', 'joule-moq-ingest', '--bin', 'wai-ingest') },
|
|
@{ dir = 'joule-code\joule-code-rs\crates\joule-moq-remote'; bin = 'wai-remote-host'; args = @('--bin', 'wai-remote-host') },
|
|
@{ dir = 'wai\wai-pack'; bin = 'wai-pack'; args = @('--bin', 'wai-pack') },
|
|
@{ dir = 'wai\wai-quantum-cli'; bin = 'wai-quantum'; args = @('--bin', 'wai-quantum') }
|
|
)
|
|
foreach ($b in $builds) {
|
|
Push-Location $b.dir
|
|
cargo build --locked --release --target $t @($b.args)
|
|
if ($LASTEXITCODE -ne 0) { throw "the build of $($b.bin) failed" }
|
|
$target = (cargo metadata --format-version 1 --no-deps --locked | ConvertFrom-Json).target_directory
|
|
Copy-Item (Join-Path $target "$t\release\$($b.bin).exe") (Join-Path $stage 'bin')
|
|
Pop-Location
|
|
}
|
|
Copy-Item -Recurse (Join-Path $env:RUNNER_TEMP 'web') (Join-Path $stage 'share\wai\web')
|
|
Copy-Item -Recurse (Join-Path $env:RUNNER_TEMP 'studio') (Join-Path $stage 'share\wai\studio')
|
|
Copy-Item wai\quickstart\README.md, wai\LICENSE $stage
|
|
& (Join-Path $stage 'bin\wai.exe') create --list
|
|
if ($LASTEXITCODE -ne 0) { throw "the packaged wai does not run" }
|
|
& (Join-Path $stage 'bin\wai-quantum.exe') --version
|
|
if ($LASTEXITCODE -ne 0) { throw "the packaged wai-quantum does not run" }
|
|
python -I wai\quickstart\test\archive_doctor.py $stage
|
|
if ($LASTEXITCODE -ne 0) { throw "wai doctor from the archive: an install or stack check did not pass" }
|
|
Compress-Archive -Path $stage -DestinationPath (Join-Path $dist "$name.zip") -Force
|
|
$h = (Get-FileHash (Join-Path $dist "$name.zip") -Algorithm SHA256).Hash.ToLower()
|
|
Set-Content -NoNewline -Path (Join-Path $dist "$name.zip.sha256") -Value "$h $name.zip`n"
|
|
- name: Attach to the release
|
|
shell: pwsh
|
|
env:
|
|
FORGE_TOKEN: ${{ github.token }}
|
|
run: |
|
|
$ErrorActionPreference = 'Stop'
|
|
$H = @{ Authorization = "token $env:FORGE_TOKEN" }
|
|
$base = "$env:FORGE_API/repos/$env:FORGE_REPO/releases"
|
|
$rel = Invoke-RestMethod -Headers $H -Uri "$base/tags/$env:GITHUB_REF_NAME"
|
|
foreach ($f in Get-ChildItem (Join-Path $env:RUNNER_TEMP 'dist')) {
|
|
foreach ($a in (Invoke-RestMethod -Headers $H -Uri "$base/$($rel.id)/assets" | Where-Object { $_.name -eq $f.Name })) {
|
|
Invoke-RestMethod -Headers $H -Method Delete -Uri "$base/$($rel.id)/assets/$($a.id)" | Out-Null
|
|
}
|
|
curl.exe -fsS -H "Authorization: token $env:FORGE_TOKEN" -X POST "$base/$($rel.id)/assets?name=$($f.Name)" -F "attachment=@$($f.FullName);type=application/octet-stream" -o NUL -w "uploaded $($f.Name): http %{http_code}`n"
|
|
if ($LASTEXITCODE -ne 0) { throw "the upload of $($f.Name) failed" }
|
|
}
|
|
|
|
formulae:
|
|
name: Homebrew formula and Scoop manifest
|
|
needs: [version, macos, linux, windows]
|
|
if: always() && needs.version.result == 'success'
|
|
runs-on: linux
|
|
steps:
|
|
- uses: actions/checkout@v5
|
|
- name: Write and attach them
|
|
shell: bash
|
|
env:
|
|
FORGE_TOKEN: ${{ github.token }}
|
|
run: |
|
|
set -euo pipefail
|
|
v="${{ needs.version.outputs.version }}"
|
|
out="$RUNNER_TEMP/formulae"; mkdir -p "$out"
|
|
python3 wai/quickstart/formulae.py "$v" "$GITHUB_REF_NAME" "$FORGE_API" "$FORGE_REPO" "$PUBLIC" "$out"
|
|
bash wai/quickstart/publish.sh "$GITHUB_REF_NAME" "wai $v" "$out"/*
|