mattermost-mobile-upstream/detox/maestro/scripts/apply_client_config_value.sh
yasser khan 93b1863b58
Some checks are pending
ci / lint-typecheck (push) Waiting to run
ci / detox-lint-typecheck (push) Waiting to run
ci / test (push) Waiting to run
ci(e2e): triage red Detox and Maestro runs against test history (#10172)
* ci(e2e): triage red Detox and Maestro runs against test history

When a Detox or Maestro lane is red, the toolkit's e2e-triage action checks
each failure against TSIO history (flaky or broken on main, the same failure on
other PRs, environment errors, or caused by this PR) and adds its verdict to the
lane's status and the job summary. Report-only unless E2E_TRIAGE_MODE is enforce.

- .github/actions/e2e-triage: the per-lane triage steps (evidence download,
  answers cache, toolkit e2e-triage) as one composite, checked out from the
  dispatch ref so the branch under test never runs with the API key.
- Each lane uploads what it saw at a failure (screenshots, the step it
  stopped at, the server's answer) as triage evidence.
- The TSIO status links name the run only when both the run id and the
  attempt are known, so triage writes only on statuses its run owns.

Toolkit pinned to bdce26e.

* ci(e2e): pin the toolkit to 24bf208; use the renamed e2e-test-* actions

The toolkit renamed e2e-triage to e2e-test-triage and e2e-status to
e2e-test-status; inputs are unchanged. Triage now holds a failure repeated on
this PR's last commits and never on the base branch as out of date (merge the
base branch and re-run) instead of clearing it, and the status line names each
kind: flaky, broken on main, caused by PR, out of date, needs investigation.

* ci(e2e): pin the toolkit to main (419b0a9)

Toolkit #5 merged; this pins its merge on main instead of the PR branch.

* ci(e2e): pin the toolkit to main (b3ca677)

Toolkit #6 merged: a run flagged as a whole now says why once instead of repeating "investigation required".

* ci(e2e): pin the toolkit to e2e-triage-judge-flagged-runs (f1f5d54, toolkit#8)

* ci(e2e): pin the toolkit to e2e-triage-judge-flagged-runs (77c3048, toolkit#8)

* ci(e2e): use the toolkit's main while triage is observed

toolkit#8 merged (b7757e9). Following review, consumers track main so
toolkit changes need no bump here; pin a hash again once stable.

---------

Co-authored-by: Mattermost Build <build@mattermost.com>
2026-10-26 10:26:45 +05:30

214 lines
9.1 KiB
Bash
Executable File

#!/bin/bash
# Apply a config patch and do not return until the *client* config actually serves the value.
#
# WHY: two config writes close together can lose the later one. In run 34344304929 the mail-mode
# step's EXIT trap restored ReportAProblemType=default at 12:03:20.977 and the link-mode step
# patched ReportAProblemType=link 58 ms later at 12:03:21.035; both curls returned success, yet the
# app observed 'default'. handleReport()'s 'default' branch emails on a licensed server, which is
# why the tap produced android.intent.action.SEND_MULTIPLE to Gmail and the browser never opened --
# across the whole job there is not one android.intent.action.VIEW. Reproduced on a live 11.11.0
# server with the value correctly in force: the same flow fires ACTION_VIEW into
# com.android.chrome and passes. So the flow and the app are fine; only the config write was lost.
#
# Patch, then poll the client config -- the payload the app consumes. A patch that was lost is
# re-applied rather than silently accepted; a value that genuinely will not stick still fails.
#
# Usage: apply_client_config_value.sh <site-url> <admin-token> <patch-json> <key> <expected-value>
# Exit 0: the client config serves <expected-value>.
# A read that matches is only meaningful because exit 3 below has already ruled out
# an installation that would answer 200 and keep its own value. Callers that need the
# value to still be in force later must check it where it matters -- this script does
# not hold it, and no wait here could prove that it will.
# Exit 3: this installation forbids the write. Checked FIRST, before any patch attempt, since
# both signals are deterministic and retrying cannot change them. Two causes are
# checked and both are printed:
# the key being supplied by an environment variable (Mattermost keeps the env value
# and silently ignores config/patch while answering 200 -- this is what the PR
# Spinwicks do, proven in run 34452126763: "RestrictSystemAdmin=false,
# set-by-environment=true"), or ExperimentalSettings.RestrictSystemAdmin being true
# (drops every write_restrictable/cloud_restrictable field). Caller should skip.
# Exit 1: the value never took for some other reason. Exit 2: usage error.
#
# CONFIG_DIAGNOSIS_FILE, when set, receives the one-line reason for exit 1 or 3, so the
# caller's failure report says why (an unreachable server, a page instead of JSON, a value
# that would not stick) instead of only that the pre-condition was not in force.
set -euo pipefail
if [[ $# -ne 5 ]]; then
echo "usage: $0 <site-url> <admin-token> <patch-json> <key> <expected-value>" >&2
exit 2
fi
site_url="$1"
admin_token="$2"
patch_json="$3"
key="$4"
expected="$5"
# Bound every request so a stalled connection cannot hang the script past its retry path.
CONNECT_TIMEOUT_SECS="${CONFIG_CONNECT_TIMEOUT_SECS:-5}"
MAX_TIME_SECS="${CONFIG_MAX_TIME_SECS:-30}"
DIAGNOSIS_FILE="${CONFIG_DIAGNOSIS_FILE:-}"
if [[ -n "$DIAGNOSIS_FILE" ]]; then
: > "$DIAGNOSIS_FILE"
fi
# A patch the client timed out on is usually still applied: config saves run one at a time and can
# take ~90 s on a busy server. Resending only queues another save behind it, so wait this long first.
TIMED_OUT_WRITE_SETTLE_SECS="${CONFIG_TIMED_OUT_WRITE_SETTLE_SECS:-240}"
# Print the final reason and keep it for the caller's failure report.
diagnose() {
echo "==> $1" >&2
if [[ -n "$DIAGNOSIS_FILE" ]]; then
printf '%s\n' "$1" > "$DIAGNOSIS_FILE"
fi
}
# What the client config serves for the key; when it can't be read, why (curl's error, or
# the start of a body that is not JSON, such as an HTML page from a proxy in front of the server).
read_key() {
local response err
err="$(mktemp)"
if ! response="$(curl -f -sS --show-error \
--connect-timeout "${CONNECT_TIMEOUT_SECS}" --max-time "${MAX_TIME_SECS}" \
-H "Authorization: Bearer ${admin_token}" \
"${site_url}/api/v4/config/client?format=old" 2>"$err")"; then
printf '<client config could not be read: %s>' "$(tr -s '\r\n' ' ' < "$err" | head -c 160)"
rm -f "$err"
return
fi
rm -f "$err"
printf '%s' "$response" | python3 -c "
import json, sys
key = sys.argv[1]
raw = sys.stdin.read()
try:
config = json.loads(raw)
except ValueError:
start = ' '.join(raw.split())[:80]
kind = 'an HTML page' if start.lower().startswith(('<!doctype html', '<html')) else 'not JSON'
print('<client config was not JSON: the server answered with %s starting %r>' % (kind, start))
sys.exit(0)
print(config.get(key, '<key absent from client config>'))
" "$key"
}
# Whether this installation will accept the write at all. Both answers are deterministic --
# neither depends on how quickly a value propagates, so they are asked before any patching
# rather than only after attempts have been spent:
# 1. ExperimentalSettings.RestrictSystemAdmin drops every write_restrictable /
# cloud_restrictable field (server/channels/api4/config.go, makeFilterConfigByPermission).
# 2. The field is supplied by an environment variable. Mattermost keeps the env value and
# silently ignores config/patch while answering 200; GET /api/v4/config/environment
# reports which fields those are. Cloud provisioners configure installations this way.
read_restricted() {
# Guard the read like read_key does. Piping a failed curl into python prints its own
# 'unknown' and then the || prints a second one, and the caller reports both.
local body
if ! body="$(curl -f -sS --show-error \
--connect-timeout "${CONNECT_TIMEOUT_SECS}" --max-time "${MAX_TIME_SECS}" \
-H "Authorization: Bearer ${admin_token}" \
"${site_url}/api/v4/config" 2>/dev/null)"; then
printf 'unknown'
return
fi
printf '%s' "$body" | python3 -c "
import json, sys
try:
config = json.load(sys.stdin)
except ValueError:
print('unknown')
sys.exit(0)
print(str(config.get('ExperimentalSettings', {}).get('RestrictSystemAdmin', 'unknown')).lower())
"
}
read_env_managed() {
local body
if ! body="$(curl -f -sS --show-error \
--connect-timeout "${CONNECT_TIMEOUT_SECS}" --max-time "${MAX_TIME_SECS}" \
-H "Authorization: Bearer ${admin_token}" \
"${site_url}/api/v4/config/environment" 2>/dev/null)"; then
printf 'unknown'
return
fi
printf '%s' "$body" | python3 -c "
import json, sys
patch = sys.argv[1]
try:
env = json.load(sys.stdin)
fields = json.loads(patch)
except ValueError:
print('unknown')
sys.exit(0)
for section, values in fields.items():
if not isinstance(values, dict):
continue
section_env = env.get(section, {})
for field in values:
if section_env.get(field):
print('true')
sys.exit(0)
print('false')
" "$patch_json"
}
# Ask first. An installation that owns the field will answer 200 to every patch and keep its
# own value, so retrying cannot help and a read that briefly matches is not evidence.
restricted="$(read_restricted)"
env_managed="$(read_env_managed)"
if [[ "$restricted" == "true" || "$env_managed" == "true" ]]; then
diagnose "${key} is owned by this installation (RestrictSystemAdmin=${restricted}, set-by-environment=${env_managed}), so the flow's pre-condition cannot be created here."
exit 3
fi
actual=""
for attempt in 1 2 3; do
rc=0
curl -f -sS --show-error -X PUT \
--connect-timeout "${CONNECT_TIMEOUT_SECS}" --max-time "${MAX_TIME_SECS}" \
-H "Authorization: Bearer ${admin_token}" \
-H "Content-Type: application/json" \
-d "$patch_json" \
"${site_url}/api/v4/config/patch" > /dev/null || rc=$?
settle_secs=10
if (( rc == 28 )); then
echo "==> config/patch timed out on attempt ${attempt}; waiting up to ${TIMED_OUT_WRITE_SETTLE_SECS}s for it to land" >&2
settle_secs="${TIMED_OUT_WRITE_SETTLE_SECS}"
elif (( rc != 0 )); then
echo "==> config/patch call failed on attempt ${attempt}" >&2
actual="<config/patch call failed>"
continue
fi
# Poll rather than read once: a clustered installation propagates a patch asynchronously.
deadline=$(( SECONDS + settle_secs ))
while :; do
actual="$(read_key)"
if [[ "$actual" == "$expected" ]]; then
echo "==> Verified ${key}=${actual} in the client config (attempt ${attempt})"
exit 0
fi
if (( SECONDS >= deadline )); then
break
fi
sleep 2
done
echo "==> ${key} is '${actual}' after attempt ${attempt}, re-applying the patch" >&2
done
# The installation said it would accept the write, so re-ask rather than assume: the answer
# can change mid-run, and printing both signals keeps the next run diagnosable.
restricted="$(read_restricted)"
env_managed="$(read_env_managed)"
diagnose "${key} never took: wanted '${expected}', client config serves '${actual}' (RestrictSystemAdmin=${restricted}, set-by-environment=${env_managed})"
if [[ "$restricted" == "true" || "$env_managed" == "true" ]]; then
echo "==> This installation does not allow that write, so the flow's pre-condition cannot be created here." >&2
exit 3
fi
exit 1