build: add reproducible release bundles
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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)")
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)")
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Executable
+116
@@ -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"
|
||||
Reference in New Issue
Block a user