build: add reproducible release bundles

This commit is contained in:
2026-09-11 09:13:36 +00:00
parent 2cf563d88e
commit cc31f6cb67
7 changed files with 180 additions and 0 deletions
+10
View File
@@ -26,12 +26,22 @@ import (
"google.golang.org/grpc"
)
// buildVersion is set by scripts/release for published artifacts. Development
// and test builds intentionally retain the explicit non-release value.
var buildVersion = "dev"
func main() {
var configPath string
var checkConfig bool
var version bool
flag.StringVar(&configPath, "config", "", "absolute server TOML configuration path")
flag.BoolVar(&checkConfig, "check-config", false, "validate server configuration and exit")
flag.BoolVar(&version, "version", false, "print build version and exit")
flag.Parse()
if version {
fmt.Fprintln(os.Stdout, buildVersion)
return
}
if configPath == "" {
log.Print("rvbox-server: --config is required")
os.Exit(2)
+7
View File
@@ -26,6 +26,9 @@ import (
"google.golang.org/protobuf/types/known/timestamppb"
)
// buildVersion is set by scripts/release for published artifacts.
var buildVersion = "dev"
func main() {
if err := run(os.Args[1:], os.Stdout, os.Stderr); err != nil {
fmt.Fprintln(os.Stderr, "rvbox:", err)
@@ -39,6 +42,10 @@ var nativeTestContextFailures map[clientwindows.ExecutionContext]bool
// --service with an explicit config path; tray/helper modes cannot silently
// turn an ordinary process invocation into a privileged service.
func run(args []string, output, diagnostics io.Writer) error {
if len(args) == 1 && args[0] == "--version" {
_, err := fmt.Fprintln(output, buildVersion)
return err
}
if len(args) == 0 {
return errors.New("an internal mode is required (use --help)")
}
+8
View File
@@ -23,6 +23,14 @@ func TestClientModeSelectionRequiresExactlyOneMode_HP_WINCLI_01(t *testing.T) {
}
}
func TestClientVersionDoesNotRequireConfiguration_HP_WINCLI_03(t *testing.T) {
t.Parallel()
var output, diagnostics bytes.Buffer
if err := run([]string{"--version"}, &output, &diagnostics); err != nil || output.String() != "dev\n" {
t.Fatalf("version = %q, %v", output.String(), err)
}
}
func TestClientHTTPClientAcceptsMatchingSelfSignedLeaf(t *testing.T) {
t.Parallel()
server := httptest.NewTLSServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
+7
View File
@@ -25,6 +25,9 @@ import (
const defaultControlSocket = "/run/rvbox/server.sock"
// buildVersion is set by scripts/release for published artifacts.
var buildVersion = "dev"
func main() {
if err := run(os.Args[1:], os.Stdout, os.Stderr); err != nil {
fmt.Fprintln(os.Stderr, "rvc:", err)
@@ -33,6 +36,10 @@ func main() {
}
func run(args []string, output, diagnostics io.Writer) error {
if len(args) == 1 && args[0] == "--version" {
_, err := fmt.Fprintln(output, buildVersion)
return err
}
if len(args) == 0 {
return errors.New("a command is required (stat or run)")
}
+8
View File
@@ -50,6 +50,14 @@ func TestGlobalRequestIDIsInjectedOnlyForMutations_HP_CTL_12(t *testing.T) {
}
}
func TestVersionDoesNotRequireControlSocket_HP_CTL_16(t *testing.T) {
t.Parallel()
var output, diagnostics bytes.Buffer
if err := run([]string{"--version"}, &output, &diagnostics); err != nil || output.String() != "dev\n" {
t.Fatalf("version = %q, %v", output.String(), err)
}
}
func TestRenderCommandStatShowsExpiryRetentionAndWindowsIdentity_HP_CTL_13(t *testing.T) {
t.Parallel()
expiry := time.Date(2026, 9, 6, 12, 0, 0, 0, time.UTC)
+24
View File
@@ -69,6 +69,30 @@ false and records an incident; do not delete segments to force readiness.
and full data directory, then start the known-good version. Preserve logs
and the failed copy for diagnosis.
## Release bundle
Build a release candidate only from a clean, committed worktree. The
containerized release wrapper embeds the supplied version in all three binaries,
creates an immutable `dist/rvbox-VERSION` directory, and writes `SHA256SUMS`
plus `manifest.json` only after the Windows executable has optionally been
signed:
```sh
scripts/release build --version 1.0.0-rc.1
sha256sum -c dist/rvbox-1.0.0-rc.1/SHA256SUMS
dist/rvbox-1.0.0-rc.1/rvbox-server-linux-amd64 --version
dist/rvbox-1.0.0-rc.1/rvc-linux-amd64 --version
```
For a Windows-signed release, provide an executable host-side signing hook via
`--sign-windows-hook /absolute/path/to/hook`. The wrapper invokes it with the
Windows executable path and version, then records `windows_signed: true` in the
manifest. Without that hook the manifest deliberately declares the artifact
unsigned; this is suitable for CI/test evidence but not a signed public
release. The wrapper never overwrites a final bundle, so correcting a failed
candidate requires choosing a new version/output or deliberately removing that
exact ignored `dist/` directory after preserving any evidence.
## Common incidents
- **No client / stale session:** verify nginx has WebSocket `101` entries for
+116
View File
@@ -0,0 +1,116 @@
#!/bin/sh
# Build an inspectable, reproducible RVBox Linux-server/Windows-client bundle.
# Publishing and certificate custody remain outside this repository; an optional
# local signing hook can sign the Windows executable before checksums are made.
set -eu
repo_root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
compose_file=$repo_root/deploy/compose.yaml
usage() {
cat <<'EOF'
usage: scripts/release build --version VERSION [--output DIR] [--sign-windows-hook FILE]
Builds a fresh immutable bundle containing:
rvbox-server-linux-amd64
rvc-linux-amd64
rvbox-windows-amd64.exe
SHA256SUMS
manifest.json
The default output is dist/rvbox-VERSION. VERSION must be a safe release label
([0-9A-Za-z][0-9A-Za-z._+-]{0,63}); an existing final output is never replaced.
Artifacts are built inside the pinned Docker toolchain with that exact version
embedded in `--version`. The optional signing hook is a regular executable run
on the host as: HOOK WINDOWS_EXE VERSION. It receives RVBOX_ARTIFACT and
RVBOX_VERSION as environment variables and must sign the supplied executable
in place. SHA256SUMS and manifest.json are generated only after it succeeds.
No signing hook means the manifest explicitly marks the Windows artifact as
unsigned. This is deliberate for CI/test builds; do not publish it as signed.
EOF
}
fail() { printf '%s\n' "release: $*" >&2; exit 2; }
command=${1:-}
[ "$#" -gt 0 ] && shift
[ "$command" = build ] || { usage >&2; fail "expected build"; }
version=
output=
sign_hook=
while [ "$#" -gt 0 ]; do
case $1 in
--version) [ "$#" -ge 2 ] || fail "--version needs a value"; version=$2; shift 2 ;;
--output) [ "$#" -ge 2 ] || fail "--output needs a directory"; output=$2; shift 2 ;;
--sign-windows-hook) [ "$#" -ge 2 ] || fail "--sign-windows-hook needs an executable file"; sign_hook=$2; shift 2 ;;
--help|-h) usage; exit 0 ;;
*) fail "unknown argument $1" ;;
esac
done
case $version in
''|[!0-9A-Za-z]*|*[!0-9A-Za-z._+-]*|?????????????????????????????????????????????????????????????????*)
fail "--version must match [0-9A-Za-z][0-9A-Za-z._+-]{0,63}"
;;
esac
if [ -z "$output" ]; then output=$repo_root/dist/rvbox-$version; fi
case $output in
/*) ;;
*) output=$repo_root/$output ;;
esac
parent=$(dirname -- "$output")
[ ! -e "$output" ] || fail "refusing to replace existing output $output"
[ -d "$parent" ] || mkdir -p "$parent"
[ ! -L "$parent" ] || fail "refusing symlink output parent $parent"
if [ -n "$sign_hook" ]; then
[ -f "$sign_hook" ] && [ ! -L "$sign_hook" ] && [ -x "$sign_hook" ] || fail "signing hook must be an executable regular file"
fi
docker version >/dev/null 2>&1 || fail "Docker is unavailable"
docker compose -f "$compose_file" version >/dev/null 2>&1 || fail "Docker Compose is unavailable"
partial=$parent/.rvbox-$version.partial-$$
mkdir "$partial" || fail "could not create private release staging directory"
cleanup() { rm -rf -- "$partial"; }
trap cleanup EXIT HUP INT TERM
commit=$(git -C "$repo_root" rev-parse HEAD)
dirty=$(git -C "$repo_root" status --porcelain)
[ -z "$dirty" ] || fail "refusing release build from a dirty worktree"
docker compose -f "$compose_file" run --rm toolchain sh -ec '
set -eu
version=$1
out=$2
flags="-s -w -X main.buildVersion=$version"
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -ldflags "$flags" -o "$out/rvbox-server-linux-amd64" ./cmd/rvbox-server
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -ldflags "$flags" -o "$out/rvc-linux-amd64" ./cmd/rvc
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -trimpath -ldflags "-H=windowsgui $flags" -o "$out/rvbox-windows-amd64.exe" ./cmd/rvbox
' sh "$version" "$partial"
signed=false
if [ -n "$sign_hook" ]; then
RVBOX_ARTIFACT=$partial/rvbox-windows-amd64.exe RVBOX_VERSION=$version "$sign_hook" "$partial/rvbox-windows-amd64.exe" "$version"
signed=true
fi
for artifact in rvbox-server-linux-amd64 rvc-linux-amd64 rvbox-windows-amd64.exe; do
[ -f "$partial/$artifact" ] && [ ! -L "$partial/$artifact" ] || fail "builder did not create regular $artifact"
done
(cd "$partial" && sha256sum rvbox-server-linux-amd64 rvc-linux-amd64 rvbox-windows-amd64.exe) >"$partial/SHA256SUMS"
{
printf '{\n'
printf ' "schema": 1,\n'
printf ' "version": "%s",\n' "$version"
printf ' "git_commit": "%s",\n' "$commit"
printf ' "windows_signed": %s,\n' "$signed"
printf ' "artifacts": "SHA256SUMS"\n'
printf '}\n'
} >"$partial/manifest.json"
chmod 0755 "$partial/rvbox-server-linux-amd64" "$partial/rvc-linux-amd64" "$partial/rvbox-windows-amd64.exe"
chmod 0644 "$partial/SHA256SUMS" "$partial/manifest.json"
mv -- "$partial" "$output"
trap - EXIT HUP INT TERM
printf 'release_bundle=%s\n' "$output"