Integrate reviewed manual update channel admission

Accept the exact Delivery candidate and its complete source-s468dla1 installer proof. The final repair removes only contract EOF whitespace; installer and regression-test bytes are unchanged. Native installed recovery and protected publication remain separate.

Change-source: pulse-maintainer
This commit is contained in:
pulse-triage[bot]
2026-10-07 19:55:33 +01:00
4 changed files with 299 additions and 4 deletions
@@ -5186,6 +5186,30 @@ curl configuration and have five-second connect/twenty-second overall bounds.
`scripts/installtests/pulse_auto_update_test.go` own this proof. Signature,
edition, consent, backup, service-restoration and installation gates remain
separate and unchanged.
### Manual update channel admission (7 October 2026)
The installed `update` helper selects a saved release channel only from one
complete `system.json` object and its top-level `updateChannel`. Missing, null
or empty channels retain the stable default; recognised strings are trimmed
and case-normalised. Nested or quoted settings cannot opt into RC. Malformed,
multiple, non-object or unknown-channel configuration, a non-regular path or
missing jq stops before downloading or executing the installer, with explicit
`--stable`/`--rc` recovery guidance and no configuration rewrite. A missing file
retains the default. Explicit existing selectors (including `--prerelease`)
and the existing source-build marker keep precedence over saved configuration.
The helper forwards both parsed channels explicitly, including the stable
default for an existing file, so the containing installer cannot reinterpret
that same configuration through its older saved-preference reader.
`TestManualUpdateChannelAdmission` generates and executes the actual helper
against local transport fixtures and genuinely signed installer/sidecar bytes.
It checks final arguments and the actual installer channel-selection functions,
pre-download refusal, configuration preservation, selector/marker precedence
and signature rejection. This is bounded source
acceptance, not installed upgrade, native service restoration or published
release acceptance. The installer, archive, signing, rollback and service gates
remain unchanged.
That same boundary also owns operator-facing management entry points for
existing self-hosted installs: the installer's printed update/reset/uninstall
commands and the active install or upgrade docs must route supported
+27 -3
View File
@@ -4042,7 +4042,7 @@ auto_selector_allowed=true
if [[ \${#helper_args[@]} -gt 0 ]]; then
for helper_arg in "\${helper_args[@]}"; do
case "\$helper_arg" in
-h|--help|--uninstall|--version|--rc|--pre|--stable|--source|--from-source|--branch|--archive|--archive=*|--skip-upgrade-preflight)
-h|--help|--uninstall|--version|--rc|--pre|--prerelease|--stable|--source|--from-source|--branch|--archive|--archive=*|--skip-upgrade-preflight)
auto_selector_allowed=false
break
;;
@@ -4055,10 +4055,34 @@ if [[ "\$auto_selector_allowed" == "true" ]]; then
if [[ -n "\$branch" ]]; then
extra_args+=(--source "\$branch")
fi
elif [[ -f "\${CONFIG_DIR}/system.json" ]]; then
configured_channel=\$(grep -o '"updateChannel"[[:space:]]*:[[:space:]]*"[^"]*"' "\${CONFIG_DIR}/system.json" 2>/dev/null | sed 's/.*"\([^"]*\)"$/\1/' || true)
elif [[ -e "\${CONFIG_DIR}/system.json" || -L "\${CONFIG_DIR}/system.json" ]]; then
# Never infer a release channel from nested, quoted or partial JSON.
# Missing/null channels retain the stable default; malformed or unknown
# preferences stop before download. An explicit selector above wins.
if [[ ! -f "\${CONFIG_DIR}/system.json" ]] || ! command -v jq >/dev/null 2>&1; then
echo "Cannot read the saved update channel; a regular system.json and jq are required. Select --stable or --rc explicitly, or restore the configuration." >&2
exit 1
fi
if ! configured_channel=\$(jq -er -s '
if length != 1 or (.[0] | type) != "object" then
error("expected one configuration object")
else .[0] end
| .updateChannel
| if . == null then "stable" else . end
| if type != "string" then error("invalid update channel") else . end
| gsub("^\\\\s+|\\\\s+\$"; "") | ascii_downcase
| if . == "" then "stable" else . end
| if . == "stable" or . == "rc" then . else error("unknown update channel") end
' "\${CONFIG_DIR}/system.json" 2>/dev/null); then
echo "Cannot parse the saved update channel; no installer downloaded. Select --stable or --rc explicitly, or restore the configuration." >&2
exit 1
fi
if [[ "\$configured_channel" == "rc" ]]; then
extra_args+=(--rc)
else
# Bind the parsed default too: the installer has its own saved
# preference reader, which must not re-interpret the same file.
extra_args+=(--stable)
fi
fi
fi
+4 -1
View File
@@ -4366,7 +4366,7 @@ func TestSetupUpdateCommandHonorsRCChannelAndCustomPaths(t *testing.T) {
if !strings.Contains(got, `helper_args=()`) || !strings.Contains(got, `helper_args=("$@")`) {
t.Fatalf("update helper missing passthrough helper args:\n%s", got)
}
if !strings.Contains(got, `-h|--help|--uninstall|--version|--rc|--pre|--stable|--source|--from-source|--branch|--archive|--archive=*|--skip-upgrade-preflight)`) {
if !strings.Contains(got, `-h|--help|--uninstall|--version|--rc|--pre|--prerelease|--stable|--source|--from-source|--branch|--archive|--archive=*|--skip-upgrade-preflight)`) {
t.Fatalf("update helper missing auto-selector guard for explicit flags:\n%s", got)
}
if !strings.Contains(got, `extra_args+=("${helper_args[@]}")`) {
@@ -4375,6 +4375,9 @@ func TestSetupUpdateCommandHonorsRCChannelAndCustomPaths(t *testing.T) {
if !strings.Contains(got, `extra_args+=(--rc)`) {
t.Fatalf("update helper missing rc channel forwarding:\n%s", got)
}
if !strings.Contains(got, `extra_args+=(--stable)`) {
t.Fatalf("update helper must bind the parsed stable channel before the installer rereads configuration:\n%s", got)
}
if !strings.Contains(got, `INSTALLER_URL="https://github.com/example/pulse-fork/releases/latest/download/install.sh"`) {
t.Fatalf("update helper missing configured repo installer url:\n%s", got)
}
@@ -0,0 +1,244 @@
package installtests
import (
"os"
"os/exec"
"path/filepath"
"reflect"
"regexp"
"strings"
"testing"
)
// Exercise the generated helper, not a second copy of its channel parser. The
// transport is local-only, but the installer and sidecar use real SSH signatures.
func TestManualUpdateChannelAdmission(t *testing.T) {
for _, tool := range []string{"bash", "jq", "ssh-keygen"} {
if _, err := exec.LookPath(tool); err != nil {
t.Fatalf("manual update proof requires %s: %v", tool, err)
}
}
source, err := filepath.Abs(filepath.Join("..", "..", "install.sh"))
if err != nil {
t.Fatal(err)
}
bash, err := exec.LookPath("bash")
if err != nil {
t.Fatal(err)
}
assets := t.TempDir()
key := filepath.Join(assets, "signing-key")
if out, err := exec.Command("ssh-keygen", "-q", "-t", "ed25519", "-N", "", "-f", key).CombinedOutput(); err != nil {
t.Fatalf("create fixture key: %v\n%s", err, out)
}
publicKey, err := os.ReadFile(key + ".pub")
if err != nil {
t.Fatal(err)
}
rootSource, err := os.ReadFile(source)
if err != nil {
t.Fatal(err)
}
selectionFunctions := ""
for _, name := range []string{"read_configured_update_channel", "selected_update_channel"} {
body := regexp.MustCompile(`(?ms)^` + name + `\(\) \{\n.*?^\}`).Find(rootSource)
if body == nil {
t.Fatalf("cannot find actual installer channel function %s", name)
}
selectionFunctions += string(body) + "\n"
}
installer := filepath.Join(assets, "install.sh")
// The real installer reads configuration again unless the helper forwards
// an explicit channel. Use its actual selection functions to catch that
// crossing rather than accepting helper arguments alone as the outcome.
fixture := `#!/bin/bash
set -eu
CONFIG_DIR="$PULSE_CONFIG_DIR"
FORCE_CHANNEL=""
FORCE_VERSION=""
UPDATE_CHANNEL=""
IGNORE_CONFIGURED_UPDATE_CHANNEL=false
` + selectionFunctions + `
for arg in "$@"; do
case "$arg" in
--stable) FORCE_CHANNEL=stable ;;
--rc|--pre|--prerelease) FORCE_CHANNEL=rc ;;
esac
done
printf '%s\n' EXECUTED "$@" > "$MANUAL_UPDATE_CALLS"
selected_update_channel > "$MANUAL_UPDATE_CHANNEL"
`
if err := os.WriteFile(installer, []byte(fixture), 0600); err != nil {
t.Fatal(err)
}
if out, err := exec.Command("ssh-keygen", "-q", "-Y", "sign", "-f", key, "-n", "pulse-install", installer).CombinedOutput(); err != nil {
t.Fatalf("sign fixture installer: %v\n%s", err, out)
}
cases := []struct {
name, config, marker string
args, want []string
blocked bool
missingConfig bool
missingJQ bool
directory bool
brokenLink bool
badSignature bool
}{
{name: "stable", config: `{"updateChannel":"stable"}`, want: []string{"--stable"}},
{name: "rc", config: `{"updateChannel":"rc"}`, want: []string{"--rc"}},
{name: "normalized_rc", config: `{"updateChannel":" RC "}`, want: []string{"--rc"}},
{name: "normalized_stable", config: `{"updateChannel":" STABLE "}`, want: []string{"--stable"}},
{name: "missing_setting", config: `{}`, want: []string{"--stable"}},
{name: "null_setting", config: `{"updateChannel":null}`, want: []string{"--stable"}},
{name: "empty_setting", config: `{"updateChannel":" "}`, want: []string{"--stable"}},
{name: "missing_file", missingConfig: true},
{name: "nested_only", config: `{"other":{"updateChannel":"rc"}}`, want: []string{"--stable"}},
{name: "stable_with_nested_rc", config: `{"updateChannel":"stable","other":{"updateChannel":"rc"}}`, want: []string{"--stable"}},
{name: "rc_with_nested_stable", config: `{"updateChannel":"rc","other":{"updateChannel":"stable"}}`, want: []string{"--rc"}},
{name: "escaped_setting_name", config: `{"update\u0043hannel":"rc"}`, want: []string{"--rc"}},
{name: "quoted_only", config: `{"note":"\"updateChannel\":\"rc\""}`, want: []string{"--stable"}},
{name: "truncated", config: `{"updateChannel":"rc"`, blocked: true},
{name: "valid_object_then_garbage", config: `{"updateChannel":"rc"} trailing`, blocked: true},
{name: "two_objects", config: `{"updateChannel":"stable"} {"updateChannel":"rc"}`, blocked: true},
{name: "empty_file", blocked: true},
{name: "array", config: `[{"updateChannel":"rc"}]`, blocked: true},
{name: "string", config: `"rc"`, blocked: true},
{name: "boolean", config: `{"updateChannel":false}`, blocked: true},
{name: "number", config: `{"updateChannel":1}`, blocked: true},
{name: "channel_array", config: `{"updateChannel":["rc"]}`, blocked: true},
{name: "unknown_channel", config: `{"updateChannel":"beta"}`, blocked: true},
{name: "missing_jq", config: `{"updateChannel":"rc"}`, missingJQ: true, blocked: true},
{name: "directory", directory: true, blocked: true},
{name: "broken_link", brokenLink: true, blocked: true},
{name: "explicit_stable_over_rc", config: `{"updateChannel":"rc"}`, args: []string{"--stable"}, want: []string{"--stable"}},
{name: "explicit_rc_over_stable", config: `{"updateChannel":"stable"}`, args: []string{"--rc"}, want: []string{"--rc"}},
{name: "explicit_prerelease_over_invalid", config: `{"updateChannel":false}`, args: []string{"--prerelease"}, want: []string{"--prerelease"}},
{name: "explicit_version_over_invalid", config: `{`, args: []string{"--version", "v6.5.0"}, want: []string{"--version", "v6.5.0"}},
{name: "explicit_source_over_invalid", config: `{`, args: []string{"--source", "main"}, want: []string{"--source", "main"}},
{name: "explicit_archive_over_invalid", config: `{`, args: []string{"--archive", "/tmp/signed-release.tar.gz"}, want: []string{"--archive", "/tmp/signed-release.tar.gz"}},
{name: "ordinary_options_keep_rc", config: `{"updateChannel":"rc"}`, args: []string{"--no-auto-update"}, want: []string{"--rc", "--no-auto-update"}},
{name: "source_marker_over_invalid", config: `{`, marker: "maintenance\n", want: []string{"--source", "maintenance"}},
{name: "explicit_channel_over_source_marker", config: `{`, marker: "main\n", args: []string{"--stable"}, want: []string{"--stable"}},
{name: "signature_still_required", config: `{"updateChannel":"rc"}`, badSignature: true, blocked: true},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
dir := t.TempDir()
configDir := filepath.Join(dir, "config")
if err := os.Mkdir(configDir, 0700); err != nil {
t.Fatal(err)
}
configPath := filepath.Join(configDir, "system.json")
switch {
case tc.directory:
err = os.Mkdir(configPath, 0700)
case tc.brokenLink:
err = os.Symlink(filepath.Join(dir, "absent"), configPath)
case !tc.missingConfig:
err = os.WriteFile(configPath, []byte(tc.config), 0600)
}
if err != nil {
t.Fatal(err)
}
if tc.marker != "" {
if err := os.WriteFile(filepath.Join(dir, "BUILD_FROM_SOURCE"), []byte(tc.marker), 0600); err != nil {
t.Fatal(err)
}
}
helper := filepath.Join(dir, "update")
generate := exec.Command(bash, "-c", `source "$INSTALLER_SOURCE"
PINNED_RELEASE_SSH_PUBLIC_KEY="$FIXTURE_PUBLIC_KEY"
setup_update_command
`)
generate.Env = append(os.Environ(), "INSTALLER_SOURCE="+source, "FIXTURE_PUBLIC_KEY="+strings.TrimSpace(string(publicKey)),
"PULSE_INSTALL_DIR="+dir, "PULSE_CONFIG_DIR="+configDir, "PULSE_UPDATE_HELPER_PATH="+helper,
"PULSE_PROFILE_PATH="+filepath.Join(dir, "profile"), "PULSE_BASHRC_PATH="+filepath.Join(dir, "bashrc"))
if out, err := generate.CombinedOutput(); err != nil {
t.Fatalf("generate helper: %v\n%s", err, out)
}
if out, err := exec.Command(bash, "-n", helper).CombinedOutput(); err != nil {
t.Fatalf("generated Bash syntax: %v\n%s", err, out)
}
curl := `#!/bin/bash
set -eu
url=""
dest=""
while [[ $# -gt 0 ]]; do
case "$1" in
-o) dest="$2"; shift 2 ;;
https://github.com/rcourtman/Pulse/releases/latest/download/install.sh*) url="$1"; shift ;;
-fsSL) shift ;;
*) exit 98 ;;
esac
done
[[ -n "$url" && -n "$dest" ]] || exit 97
printf '%s\n' "$url" >> "$MANUAL_UPDATE_TRANSPORT"
case "$url" in
*.sshsig) if [[ "$BAD_SIGNATURE" == true ]]; then printf invalid > "$dest"; else cp "$FIXTURE_ASSETS/install.sh.sig" "$dest"; fi ;;
*/install.sh) cp "$FIXTURE_ASSETS/install.sh" "$dest" ;;
*) exit 96 ;;
esac
`
if err := os.WriteFile(filepath.Join(dir, "curl"), []byte(curl), 0700); err != nil {
t.Fatal(err)
}
path := dir + string(os.PathListSeparator) + os.Getenv("PATH")
if tc.missingJQ {
path = dir // parser fails before any non-builtin operation
}
calls := filepath.Join(dir, "calls")
transport := filepath.Join(dir, "transport")
channel := filepath.Join(dir, "channel")
cmd := exec.Command(bash, append([]string{helper}, tc.args...)...)
cmd.Env = append(os.Environ(), "PATH="+path, "FIXTURE_ASSETS="+assets, "MANUAL_UPDATE_CALLS="+calls, "MANUAL_UPDATE_TRANSPORT="+transport, "MANUAL_UPDATE_CHANNEL="+channel,
"BAD_SIGNATURE="+map[bool]string{true: "true", false: "false"}[tc.badSignature])
out, runErr := cmd.CombinedOutput()
if tc.blocked {
if runErr == nil {
t.Fatalf("invalid preference/signature admitted: %s", out)
}
if _, err := os.Stat(calls); !os.IsNotExist(err) {
t.Fatalf("rejected helper executed installer: %v\n%s", err, out)
}
if !tc.badSignature {
if _, err := os.Stat(transport); !os.IsNotExist(err) {
t.Fatalf("invalid preference reached download: %v\n%s", err, out)
}
if !strings.Contains(string(out), "saved update channel") || !strings.Contains(string(out), "--stable or --rc") {
t.Fatalf("missing safe recovery guidance: %s", out)
}
}
} else {
if runErr != nil {
t.Fatalf("valid helper failed: %v\n%s", runErr, out)
}
got, err := os.ReadFile(calls)
if err != nil {
t.Fatal(err)
}
want := append([]string{"EXECUTED"}, tc.want...)
if !reflect.DeepEqual(strings.Split(strings.TrimSuffix(string(got), "\n"), "\n"), want) {
t.Fatalf("installer args %q, want %q", got, want)
}
selected, err := os.ReadFile(channel)
wantChannel := "stable"
for _, arg := range tc.want {
if arg == "--rc" || arg == "--prerelease" {
wantChannel = "rc"
}
}
if err != nil || strings.TrimSpace(string(selected)) != wantChannel {
t.Fatalf("installer selected %q: %v, want %s", selected, err, wantChannel)
}
}
if !tc.missingConfig && !tc.directory && !tc.brokenLink {
got, err := os.ReadFile(configPath)
if err != nil || string(got) != tc.config {
t.Fatalf("helper changed configuration: %v, %q", err, got)
}
}
})
}
}