Files
LocalAI/scripts/addons/package_test.sh
T
Ettore Di Giacinto 02a707558e feat(addons): ship Studio and Talk as add-ons instead of built-in pages
Studio and Talk were compiled into the web bundle and always listed in
the Add-ons drawer. They are add-ons now, like Voice, Face, fine-tuning
and quantization: a person installs and removes them, the drawer lists
them only while they are installed, and they are packaged the same way.

Move the pages, their components, hooks and helpers into addons/studio
and addons/talk. Each has a manifest, a UI bundle, a release platform
matrix and a catalog template, and is built by the compat mode of the
shared builder. The core API stays where it is: the pages call it with
the signed-in person's session. The history keeps its browser storage
keys. The unit tests of the moved helpers move with them.

In the core, drop the two pages, the Create group of the sidebar (Chat
joins Home and Models in the old layout), the built-in catalog entries
(the catalog is empty) and the talk locale namespace. The old addresses
(/app/studio, /app/talk and the generator links) redirect to the add-on
pages, with the model as ?model=. The Home /studio command is offered
only while the Studio add-on is installed and opens it.

The release workflow and its tests now list seven add-ons.

Assisted-by: Claude Code:claude-sonnet-5-5 go node
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-10-20 21:40:00 +00:00

173 lines
11 KiB
Bash
Executable File

#!/bin/sh
# SPDX-License-Identifier: MIT
# Packaging checks of the first-party add-on OCI package. It prints one JSON
# line per case; scripts/addons/verify.sh compares the lines with the exact
# inventory of the "packaging" suite and rejects a skipped or missing case.
#
# 1. Go specs of pkg/addons/packaging: layout rules, rejected packages,
# event and channel policy, merge completeness, publication and alias
# promotion against an in-process registry, catalog template, and the
# workflow policy with one failing mutant per rule.
# 2. The installer contract: the package built by package.sh is installed
# by the real installer from a real local OCI distribution and its
# executable is started by the real supervisor.
# 3. Shell entry points: package.sh, channel.sh, the workflow policy
# command and static checks that nothing here pushes, tags or signs.
#
# Nothing here pushes, tags, signs or logs in: registries are loopback
# distributions inside the test process and signatures come from a fixture
# authority.
set -eu
root=$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)
cd "$root"
cache="$root/.cache/addons"
mkdir -p "$cache/tmp" "$cache/logs"
export TMPDIR="$cache/tmp"
export PATH="$cache/bin:$PATH"
export GOTOOLCHAIN=local GOPROXY=off GOSUMDB=off GOFLAGS=-mod=readonly
export GOPATH="$cache/go" GOMODCACHE="$cache/go/pkg/mod" GOCACHE="$cache/go-build"
work=$(mktemp -d "$cache/tmp/packaging.XXXXXX")
trap 'chmod -R u+rwx "$work" 2>/dev/null; rm -rf "$work"' EXIT HUP INT TERM
# The UI is a build output of this checkout; build it so the package holds
# the current files.
node addons/tables/ui/build.mjs >"$cache/logs/packaging-ui-build.log" 2>&1 || { cat "$cache/logs/packaging-ui-build.log" >&2; exit 1; }
go_cases() {
python3 -I - "$1" <<'PY'
import json, sys
for report in json.load(open(sys.argv[1])):
for spec in report['SpecReports']:
if spec['LeafNodeType'] != 'It' or spec['State'] == 'skipped' and not spec.get('Failure'):
continue
status = {'passed': 'passed'}.get(spec['State'], 'failed')
print(json.dumps({'case': 'go: ' + spec['LeafNodeText'], 'status': status}))
PY
}
run_go() {
name=$1
shift
report="$cache/logs/packaging-$name.json"
rm -f "$report"
go test -count=1 -p 2 -tags auth "$@" -ginkgo.json-report="$report" >"$cache/logs/packaging-$name.log" 2>&1 || { tail -60 "$cache/logs/packaging-$name.log" >&2; exit 1; }
go_cases "$report"
}
run_go specs ./pkg/addons/packaging
run_go installer ./tests/addons/packaging
# Shell entry points.
tool="$work/addonpkg"
go build -o "$tool" ./scripts/addons/cmd/addonpkg
export ADDONPKG="$tool"
case_ok() { printf '{"case":"%s","status":"passed"}\n' "$1"; }
expect_fail() { # name expected-text command...
name=$1; expected=$2; shift 2
if "$@" >"$work/out" 2>&1; then echo "FAIL $name: unexpectedly succeeded" >&2; cat "$work/out" >&2; exit 1; fi
grep -F -- "$expected" "$work/out" >/dev/null || { echo "FAIL $name: wrong failure (expected $expected)" >&2; cat "$work/out" >&2; exit 1; }
case_ok "$name"
}
sha=0123456789abcdef0123456789abcdef01234567
host="$(go env GOOS)/$(go env GOARCH)"
printf '#!/bin/sh\nexit 2\n' >"$work/stub"
chmod 755 "$work/stub"
# package.sh: a local check layout for the host builds and verifies.
sh scripts/addons/package.sh --layout "$work/leg" --version 1.2.3 --platform "$host" --executable "$work/stub" --local-check --revision "$sha" >"$work/out" 2>&1 || { cat "$work/out" >&2; exit 1; }
grep -F 'publishable=false' "$work/out" >/dev/null && grep -F 'verified=tables 1.2.3' "$work/out" >/dev/null
test -f "$work/leg/receipt.json" && test -f "$work/leg/index.json" && test -f "$work/leg/oci-layout"
case_ok 'package.sh builds and verifies a local check layout'
expect_fail 'package.sh refuses a platform the matrix has not enabled' 'not enabled' sh scripts/addons/package.sh --layout "$work/leg2" --version 1.2.3 --platform "$host" --executable "$work/stub"
expect_fail 'package.sh refuses an existing non-empty layout' 'not empty' sh scripts/addons/package.sh --layout "$work/leg" --version 1.2.3 --platform "$host" --executable "$work/stub" --local-check
expect_fail 'package.sh requires its arguments' 'usage:' sh scripts/addons/package.sh --version 1.2.3
expect_fail 'package.sh refuses an invalid version' 'semantic version' sh scripts/addons/package.sh --layout "$work/leg3" --version v1 --platform "$host" --executable "$work/stub" --local-check
# package.sh reads the add-on from its platform file. The voice add-on is built
# for the host as a local check, and its layout verifies.
sh scripts/addons/package.sh --config addons/voice/package/platforms.yaml --layout "$work/voice" --version 1.2.3 --platform "$host" --local-check --revision "$sha" >"$work/out" 2>&1 || { cat "$work/out" >&2; exit 1; }
grep -F 'verified=voice 1.2.3' "$work/out" >/dev/null
test -f "$work/voice/receipt.json"
case_ok 'package.sh builds and verifies the voice add-on for the host'
# A platform other than the host is refused for every add-on, whatever the config.
if [ "$host" = linux/amd64 ]; then foreign=linux/arm64; else foreign=linux/amd64; fi
for id in tables voice face fine-tuning quantization studio talk; do
expect_fail "package.sh refuses a foreign platform without an executable: $id" 'is not this host' sh scripts/addons/package.sh --config "addons/$id/package/platforms.yaml" --layout "$work/foreign-$id" --version 1.2.3 --platform "$foreign" --local-check
done
case_ok 'package.sh refuses a foreign platform without an executable for every add-on'
# The build line: cgo for Tables, none for the pure Go add-ons. A stub go
# records the environment and the package it was asked to build.
realgo=$(command -v go)
mkdir "$work/stubbin"
cat >"$work/stubbin/go" <<STUB
#!/bin/sh
if [ "\$1" = build ]; then
out=
for a in "\$@"; do
if [ "\$prev" = -o ]; then out=\$a; fi
prev=\$a
last=\$a
done
printf '%s CGO_ENABLED=%s\n' "\$last" "\${CGO_ENABLED-unset}" >>"$work/gobuild.log"
printf '#!/bin/sh\nexit 2\n' >"\$out"
chmod 755 "\$out"
exit 0
fi
exec "$realgo" "\$@"
STUB
chmod 755 "$work/stubbin/go"
: >"$work/gobuild.log"
for id in tables voice face fine-tuning quantization studio talk; do
PATH="$work/stubbin:$PATH" sh scripts/addons/package.sh --config "addons/$id/package/platforms.yaml" --layout "$work/stub-$id" --version 1.2.3 --platform "$host" --local-check >"$work/out" 2>&1 || { cat "$work/out" >&2; exit 1; }
done
grep -x './addons/tables/cmd/tables CGO_ENABLED=1' "$work/gobuild.log" >/dev/null
for id in voice face fine-tuning quantization studio talk; do
grep -x "./addons/$id/cmd/$id CGO_ENABLED=0" "$work/gobuild.log" >/dev/null || { echo "FAIL: $id is not built with CGO_ENABLED=0" >&2; cat "$work/gobuild.log" >&2; exit 1; }
done
case_ok 'package.sh builds Tables with cgo and the pure Go add-ons without'
expect_fail 'matrix fails closed while no platform is enabled' 'no enabled platform' "$tool" matrix --config addons/tables/package/platforms.yaml
expect_fail 'matrix fails closed without a file' 'platform matrix' "$tool" matrix --config "$work/absent.yaml"
# channel.sh.
channel() { sh scripts/addons/channel.sh "$@"; }
out=$(channel --event pull_request --ref refs/pull/7/merge --sha "$sha")
printf '%s\n' "$out" | grep -x 'publish=false' >/dev/null && printf '%s\n' "$out" | grep -x 'aliases=' >/dev/null
case_ok 'channel.sh: a pull request never publishes'
out=$(channel --event push --ref refs/heads/master --sha "$sha")
printf '%s\n' "$out" | grep -x 'aliases=development' >/dev/null && printf '%s\n' "$out" | grep -x 'immutable=sha-0123456789ab' >/dev/null
case_ok 'channel.sh: master publishes development'
out=$(channel --event push --ref refs/heads/latest --sha "$sha")
printf '%s\n' "$out" | grep -x 'publish=false' >/dev/null && printf '%s\n' "$out" | grep -x 'aliases=' >/dev/null && printf '%s\n' "$out" | grep -x 'channel=' >/dev/null
case_ok 'channel.sh: a branch push never publishes, even a branch named latest'
out=$(channel --event push --ref refs/tags/v1.2.3 --sha "$sha")
printf '%s\n' "$out" | grep -x 'aliases=' >/dev/null && printf '%s\n' "$out" | grep -x 'immutable=1.2.3' >/dev/null
case_ok 'channel.sh: a tag push prepares the version and moves no alias'
out=$(channel --event release --ref refs/tags/v1.2.3 --sha "$sha")
printf '%s\n' "$out" | grep -x 'aliases=latest' >/dev/null
case_ok 'channel.sh: a published stable release promotes latest'
out=$(channel --event release --ref refs/tags/v1.2.3 --sha "$sha" --prerelease)
printf '%s\n' "$out" | grep -x 'aliases=' >/dev/null
case_ok 'channel.sh: a pre-release never promotes latest'
expect_fail 'channel.sh: a draft release fails' 'draft' channel --event release --ref refs/tags/v1.2.3 --sha "$sha" --draft
expect_fail 'channel.sh: an unknown event fails' 'not allowed to publish' channel --event workflow_dispatch --ref refs/heads/master --sha "$sha"
expect_fail 'channel.sh: a tag that is not a version fails' 'release tag' channel --event push --ref refs/tags/latest --sha "$sha"
# Workflow policy.
"$tool" policy --workflow .github/workflows/addons-package.yml >"$work/out" 2>&1 || { cat "$work/out" >&2; exit 1; }
case_ok 'workflow policy accepts the checked-in workflow'
sed 's/^ release:$/ pull_request_target:\n types: [opened]\n release:/' .github/workflows/addons-package.yml >"$work/mutant.yml"
expect_fail 'workflow policy rejects pull_request_target' 'P_PR_TARGET' "$tool" policy --workflow "$work/mutant.yml"
sed 's/^ needs: \[plan, validate, package\]$/ needs: [plan, package]/' .github/workflows/addons-package.yml >"$work/mutant.yml"
expect_fail 'workflow policy rejects a publish job that does not wait for every leg' 'P_NEEDS' "$tool" policy --workflow "$work/mutant.yml"
sed 's/^ name: validate package rules$/ name: validate package rules\n continue-on-error: true/' .github/workflows/addons-package.yml >"$work/mutant.yml"
expect_fail 'workflow policy rejects a validate job that cannot stop the release' 'P_VALIDATE_GATE' "$tool" policy --workflow "$work/mutant.yml"
# Nothing in the packaging scripts pushes, tags, signs or logs in.
if grep -n -E 'docker push|docker login|git tag|git push|oras (push|copy)|cosign (sign|attest)|crane push|gh release' scripts/addons/package.sh scripts/addons/channel.sh scripts/addons/package_test.sh | grep -v '^[^:]*:[0-9]*:#' | grep -v 'grep -n -E' | grep -v 'static checks'; then
echo 'FAIL: a packaging script pushes, tags or signs' >&2
exit 1
fi
case_ok 'scripts never push, tag, sign or log in'
if grep -n -i -E 'quay\.io|go-skynet|backend_merge|backend_build|backend\.yml|ci-cache|docker\.io' .github/workflows/addons-package.yml scripts/addons/package.sh scripts/addons/channel.sh addons/*/package/platforms.yaml gallery/addons/tables.yaml.tmpl; then
echo 'FAIL: a packaging file references the backend pipeline' >&2
exit 1
fi
case_ok 'packaging files never reference backend registries or workflows'