ci(install): gate the remote install path end to end (#906)

* ci(install): gate the remote install path end to end

A curl|bash user takes a different path than the local install gate
covers: bootstrap outside a checkout, tag selection, clone/fetch of the
selected tag, then handoff to the tagged installer with the original
arguments. Add scripts/install-remote-test.sh, which publishes HEAD as a
tag in a local mirror, runs install.sh against it twice (fresh clone,
then fetch+reset of the existing clone), and fails closed when the
clone's HEAD is not the selected tag commit. install.sh gains a
VOCALINUX_REPO_URL override so the mirror can stand in for the public
remote. Wire a single-distro job into the distro matrix and a just
remote-install-gate recipe.

Refs #701

* ci(install): publish the gate tag as an orphan from HEAD's tree

CI checks out with fetch-depth 1, and git refuses to push or fetch a
shallow boundary commit into a ref. Build the tag on an orphan commit
created from HEAD's tree instead, and make tree equality the fail-closed
check that installer, modules and exports are this commit's files.

* test(install): force the re-run arm to prove fetch and reset

Review feedback on the remote-install gate: the second run reused a clone
already on the tag, so a broken fetch or reset would still pass. Move the
clone onto a descendant commit and delete its local tag ref before the
re-run, so only a real fetch + reset restores them. Also list the new
recipe in AGENTS.md's command guide.

---------

Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
This commit is contained in:
devin-ai-integration[bot]
2026-10-07 17:12:47 -07:00
committed by GitHub
co-authored by Jatin K Malik
parent 6a6cc93919
commit ff57244dce
6 changed files with 484 additions and 1 deletions
+29
View File
@@ -90,3 +90,32 @@ jobs:
-v "$PWD:$PWD:ro" -e REPO="$PWD" \
"${{ matrix.container }}" \
bash "$PWD/scripts/install-test.sh"
# The install matrix gates the local path: it hands install.sh a checkout.
# A curl|bash user takes a different one — bootstrap outside a checkout, tag
# selection, clone, handoff to the tagged installer — which scripts/
# install-test.sh's own header admits it does not cover (#701). One container:
# the remote-only half (bootstrap, tag, handoff) is distro-agnostic, and the
# tagged installer's per-distro work is already covered by the matrix above.
install-remote:
name: remote install.sh on debian:12
runs-on: ubuntu-latest
needs: syntax
# Two sequential remote installs (fresh clone, then fetch+reset) plus a
# real package install on each.
timeout-minutes: 45
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
# The gate publishes HEAD as a tag in its mirror; it needs the
# commit's objects, not history.
fetch-depth: 1
- name: Run the remote install path in debian:12
run: |
set -euo pipefail
docker run --rm \
-v "$PWD:$PWD:ro" -e REPO="$PWD" \
debian:12 \
bash "$PWD/scripts/install-remote-test.sh"
+1
View File
@@ -93,6 +93,7 @@ just native-packages # build the .deb and .rpm in the pinned base image (needs
just native-smoke fedora:42 # install the built package in a distro container
just aur-gate # build the AUR PKGBUILD on current Arch (needs docker)
just install-gate debian:12 # run install.sh unattended in a distro container
just remote-install-gate debian:12 # remote curl|bash path end to end against HEAD (needs docker)
just verify-release # check a published release as published (needs gh)
just pre-commit # pre-commit run --all-files
just run-debug # vocalinux --debug
+3 -1
View File
@@ -486,7 +486,9 @@ resolve_install_tag() {
resolve_install_tag
# Check if running from within the vocalinux repo or remotely (via curl)
REPO_URL="https://github.com/VocaHQ/vocalinux.git"
# VOCALINUX_REPO_URL overrides the clone source so the remote-install gate can
# serve a tag from a local mirror; public installs use the repository below.
REPO_URL="${VOCALINUX_REPO_URL:-https://github.com/VocaHQ/vocalinux.git}"
INSTALL_DIR=""
CLEANUP_ON_EXIT="${VOCALINUX_REMOTE_INSTALL:-no}"
+17
View File
@@ -151,6 +151,23 @@ install-gate distro="debian:12":
fi
docker run "${ARGS[@]}" {{distro}} bash "$PWD/scripts/install-test.sh"
# Run install.sh's remote path end to end in a distro container, as the CI gate
# does: bootstrap outside a checkout, tag selection, clone/fetch of a mirror
# published from HEAD, handoff to the tagged installer. Answers "does the
# public curl|bash path install this commit" (needs docker). Same worktree
# handling as install-gate above.
#
# Usage: `just remote-install-gate` for debian:12, or `just remote-install-gate fedora:42`
remote-install-gate distro="debian:12":
#!/usr/bin/env bash
set -euo pipefail
COMMON="$(cd "$(git rev-parse --git-common-dir)" && pwd)"
ARGS=(--rm -v "$PWD:$PWD:ro" -e REPO="$PWD")
if [ "$COMMON" != "$PWD/.git" ]; then
ARGS+=(-v "$COMMON:$COMMON:ro")
fi
docker run "${ARGS[@]}" {{distro}} bash "$PWD/scripts/install-remote-test.sh"
# Check that a published release verifies as published: manifest, provenance,
# notes and PyPI digests. Needs gh, downloads nothing.
# Usage: `just verify-release` for the latest, or `just verify-release v0.17.0`
+281
View File
@@ -0,0 +1,281 @@
#!/usr/bin/env bash
# Run install.sh's remote path end to end in a distro container and check what
# it installed.
#
# Usage (a read-only mount is enough; the tag is published from git, not the
# tree):
# docker run --rm -v "$PWD:/repo:ro" debian:12 bash /repo/scripts/install-remote-test.sh
# or: just remote-install-gate debian:12
#
# The path under test is the one a `curl | bash` user takes and
# scripts/install-test.sh never reaches: bootstrap outside a checkout, --tag
# selection, clone of the selected tag, handoff to the tagged installer with
# the original arguments plus the remote venv path, then a second run's
# fetch+reset over the existing clone.
#
# The remote is a local mirror so the gate answers for *this commit*: the tag
# is published from HEAD's tree and VOCALINUX_REPO_URL points the bootstrap at
# it. A gate that cloned the public repo would install the last release, not
# the checkout under test. The tag's own install.sh is the bootstrap: the #701
# guardrail allows the bootstrap to come from main, and this commit's copy is
# what a merged PR would serve anyway.
#
# Flags the installer sees: --auto (no TTY), --skip-models (a model is 40-75 MB
# per run and the download path is checksum-verified elsewhere), --tag (the
# selection under test). Not --skip-system-deps: the per-distro package
# installation is the point.
#
# Fail closed: after the install, the clone's tree must equal HEAD's tree —
# the #701 guardrail that installer, sourced modules and requirement exports
# all come from the selected tag is only as strong as that equality.
set -euo pipefail
REPO="${REPO:-/repo}"
INSTALL_USER="${INSTALL_USER:-installer}"
INSTALL_HOME="/home/$INSTALL_USER"
# Opaque on purpose: a name the public repository cannot already carry, so a
# silent fallback to GitHub fails loudly instead of installing the wrong ref.
GATE_TAG="${GATE_TAG:-v0.0.0-remote-gate}"
MIRROR="/srv/vocalinux-remote.git"
BOOTSTRAP="/tmp/vl.sh"
CLONE_DIR="$INSTALL_HOME/.local/share/vocalinux-install"
REMOTE_VENV="$INSTALL_HOME/.local/share/vocalinux/venv"
fail() {
echo "FAIL: $*" >&2
dump_install_log
exit 1
}
# install.sh writes a full transcript under ~/.local/state/vocalinux/. It is the
# only place the per-distro package manager output survives.
dump_install_log() {
local log
log="$(ls -1t "$INSTALL_HOME"/.local/state/vocalinux/install-*.log 2>/dev/null | head -n1 || true)"
if [ -n "$log" ]; then
echo "== last 200 lines of $log ==" >&2
tail -n 200 "$log" >&2
else
echo "== no install log was written ==" >&2
fi
}
# A mirror going slow mid-transaction leaves a half-populated cache, so
# re-running the same transaction resumes rather than restarts.
retry() {
local attempt
for attempt in 1 2 3; do
"$@" && return 0
echo " '$1' failed (attempt $attempt); retrying" >&2
sleep 5
done
return 1
}
[ -f "$REPO/install.sh" ] || fail "$REPO/install.sh not found; mount the checkout at \$REPO"
. /etc/os-release
echo "== ${NAME:-unknown} ${VERSION_ID:-} =="
echo "== Bootstrap: sudo, git and an unprivileged user =="
# The minimum for install.sh to start, and nothing it should install itself.
# git is for the mirror below; the remote path ensures it again for real users.
# Same arms as scripts/install-test.sh so `just remote-install-gate <distro>`
# works everywhere the local gate does.
case "$ID" in
ubuntu | debian)
export DEBIAN_FRONTEND=noninteractive
retry apt-get update -qq
retry apt-get install -y -qq sudo git ca-certificates >/dev/null
;;
fedora | rhel | centos)
retry dnf install -y -q sudo git shadow-utils >/dev/null
;;
arch)
# -Syu, never -Sy: archlinux:latest lags the mirrors, and a partial upgrade
# would be this script's breakage rather than the installer's.
retry pacman -Syu --noconfirm --needed sudo git >/dev/null
;;
opensuse-tumbleweed | opensuse-leap | sles)
retry zypper --non-interactive --gpg-auto-import-keys refresh >/dev/null
retry zypper --non-interactive install -y sudo git >/dev/null
;;
*)
fail "no bootstrap arm for ID='$ID'; add one when adding the container"
;;
esac
id -u "$INSTALL_USER" >/dev/null 2>&1 || useradd -m -s /bin/bash "$INSTALL_USER"
echo "$INSTALL_USER ALL=(ALL) NOPASSWD: ALL" >"/etc/sudoers.d/$INSTALL_USER"
chmod 0440 "/etc/sudoers.d/$INSTALL_USER"
echo "== Publish HEAD's tree as tag $GATE_TAG in a local mirror =="
# safe.directory: the mounted checkout and, for a linked worktree, its mounted
# common gitdir are owned by another uid.
git config --global --add safe.directory '*'
# CI checks out with fetch-depth 1, and a shallow boundary commit can neither
# be pushed nor fetched into a ref ("shallow update not allowed"). The tag
# therefore carries an orphan commit built from HEAD's tree: a root commit is
# not a shallow root, and tree equality below still binds the gate to this
# commit's exact contents.
SOURCE_TREE="$(git -C "$REPO" rev-parse 'HEAD^{tree}')" \
|| fail "$REPO is not a git checkout; the gate publishes the tag from git"
git clone --quiet --depth 1 "file://$REPO" /tmp/gate-source
# commit-tree needs an identity the bare container does not carry.
EXPECTED_SHA="$(GIT_AUTHOR_NAME=gate GIT_AUTHOR_EMAIL=gate@local \
GIT_COMMITTER_NAME=gate GIT_COMMITTER_EMAIL=gate@local \
git -C /tmp/gate-source commit-tree "$SOURCE_TREE" -m "remote-install gate")"
git init --bare "$MIRROR" >/dev/null
git -C /tmp/gate-source push "$MIRROR" "$EXPECTED_SHA:refs/tags/$GATE_TAG" >/dev/null
[ "$(git -C "$MIRROR" rev-parse "$GATE_TAG^{tree}")" = "$SOURCE_TREE" ] \
|| fail "the mirror's $GATE_TAG does not carry HEAD's tree"
# The installer clones as $INSTALL_USER, and git refuses a root-owned remote
# for anyone else as "dubious ownership".
chown -R "$INSTALL_USER:$INSTALL_USER" "$MIRROR"
# A remote user downloads install.sh over HTTPS; the gate takes the same file
# out of the tag instead. The tagged copy is this commit's bootstrap, and the
# handoff target is identical either way.
git -C "$MIRROR" show "$GATE_TAG:install.sh" >"$BOOTSTRAP"
chmod 0644 "$BOOTSTRAP"
run_remote_install() {
# cd to $INSTALL_HOME so the bootstrap sees no checkout: local mode is chosen
# by a pyproject.toml naming vocalinux in the working directory, and the
# remote path is the one under test.
su - "$INSTALL_USER" -c "cd '$INSTALL_HOME' && VOCALINUX_REPO_URL='file://$MIRROR' bash '$BOOTSTRAP' --auto --skip-models '--tag=$GATE_TAG'"
}
assert_remote_install() {
echo "== Assert: the clone is exactly the selected tag =="
[ -d "$CLONE_DIR/.git" ] || fail "remote install left no clone at $CLONE_DIR"
local cloned_sha cloned_tree
cloned_sha="$(git -C "$CLONE_DIR" rev-parse HEAD)" \
|| fail "the clone at $CLONE_DIR is not a git checkout"
[ "$cloned_sha" = "$EXPECTED_SHA" ] || fail \
"installed from the wrong revision: clone HEAD is $cloned_sha, $GATE_TAG is $EXPECTED_SHA"
[ "$(git -C "$CLONE_DIR" rev-parse "$GATE_TAG^{commit}" 2>/dev/null || true)" = "$EXPECTED_SHA" ] \
|| fail "the clone does not carry the selected tag ref $GATE_TAG"
# The tag's commit is an orphan; tree equality is the check that installer,
# modules and exports are this commit's files and not a neighbour revision's.
cloned_tree="$(git -C "$CLONE_DIR" rev-parse 'HEAD^{tree}')"
[ "$cloned_tree" = "$SOURCE_TREE" ] || fail \
"the clone's tree $cloned_tree is not HEAD's tree $SOURCE_TREE"
# The artifacts the #701 guardrail names: the tagged installer, its sourced
# modules, and the exports pip installs with --require-hashes.
[ -f "$CLONE_DIR/install.d/system_dependencies.sh" ] \
|| fail "the clone ships no install.d/system_dependencies.sh for the tagged installer to source"
[ -f "$CLONE_DIR/requirements/runtime.txt" ] \
|| fail "the clone ships no requirements/runtime.txt for the tagged installer to install"
# Same shape as the local gate's #736 check, now reachable through the remote
# path: --skip-models means the run itself never reads the manifest.
[ -f "$CLONE_DIR/src/vocalinux/utils/model_checksums.txt" ] \
|| fail "the tag ships no src/vocalinux/utils/model_checksums.txt; install.sh pins model downloads against it"
echo "== Assert: the handoff carried the original arguments =="
# handoff_to_tagged_installer appends --venv-dir; a venv anywhere else means
# the appended argument was dropped.
[ -x "$REMOTE_VENV/bin/python" ] \
|| fail "no remote venv at $REMOTE_VENV; the handoff's --venv-dir did not reach the tagged installer"
# The remote marker relocates the activation helper to ~/.local/bin only when
# VOCALINUX_REMOTE_INSTALL survives the exec.
[ -f "$INSTALL_HOME/.local/bin/activate-vocalinux.sh" ] \
|| fail "no ~/.local/bin/activate-vocalinux.sh; the remote marker did not survive the handoff"
# --skip-models forwarded: a model in the data dir means the tagged installer
# re-parsed arguments the handoff did not deliver.
if [ -d "$INSTALL_HOME/.local/share/vocalinux/models" ]; then
[ -z "$(find "$INSTALL_HOME/.local/share/vocalinux/models" -type f -print -quit)" ] \
|| fail "models were downloaded; --skip-models did not survive the handoff"
fi
echo "== Assert: the installed app is there and answers =="
[ -x "$INSTALL_HOME/.local/bin/vocalinux" ] || fail "no wrapper at ~/.local/bin/vocalinux"
[ -x "$INSTALL_HOME/.local/bin/vocalinux-gui" ] || fail "no wrapper at ~/.local/bin/vocalinux-gui"
[ -f "$INSTALL_HOME/.local/share/applications/vocalinux.desktop" ] \
|| fail "no desktop entry; install_desktop_entry did not run"
# The version the wrapper reports must be the tag's own, not an assertion
# about a version string composed locally.
local expected_version version_output
expected_version="$(grep -oP '__version__\s*=\s*"\K[^"]+' "$CLONE_DIR/src/vocalinux/version.py")" \
|| fail "cannot read __version__ from the clone's version.py"
version_output="$(su - "$INSTALL_USER" -c "'$INSTALL_HOME/.local/bin/vocalinux' --version")" \
|| fail "the installed wrapper cannot report a version"
case "$version_output" in
*"$expected_version"*) ;;
*) fail "wrapper reports '$version_output', expected the tag's $expected_version" ;;
esac
echo " vocalinux --version: $version_output"
# Same blind spots as the local gate: the injection binaries are reached
# through subprocess, and the IBus runtime only where the package list
# promises it.
for tool in xclip xsel wl-copy; do
command -v "$tool" >/dev/null 2>&1 || fail "$tool is not on PATH; install.sh lists it for every distro"
done
case "$ID" in
arch | opensuse-tumbleweed | opensuse-leap | sles)
command -v ibus-daemon >/dev/null 2>&1 \
|| fail "no ibus-daemon: the package list ships the IBus typelib without the runtime ibus_engine.py spawns"
;;
esac
su - "$INSTALL_USER" -c "'$REMOTE_VENV/bin/python' - " <<'PY' || fail "the installation does not import"
import vocalinux.main
import vocalinux.ui.tray_indicator
import vocalinux.speech_recognition.recognition_manager
import vocalinux.text_injection.text_injector
from vocalinux.ui.keyboard_backends import EVDEV_AVAILABLE, PYNPUT_AVAILABLE
import gi
# keyboard_backends imports each backend under `except ImportError`, so the
# import above stays clean when evdev and pynput both failed to build. Assert
# the flags rather than the import.
assert EVDEV_AVAILABLE or PYNPUT_AVAILABLE, (
"no keyboard backend is importable: evdev and pynput both failed to install"
)
# The typelibs the installer is responsible for putting on the system. Both are
# loaded lazily at runtime, so importing the modules above proves nothing about
# them: a missing one would surface on a user's first dictation instead.
gi.require_version("Gtk", "3.0")
gi.require_version("IBus", "1.0")
from gi.repository import Gtk, IBus # noqa: F401
print(
" main, tray, recognition, injection, Gtk and IBus import;"
f" keyboard backends: evdev={EVDEV_AVAILABLE} pynput={PYNPUT_AVAILABLE}"
)
PY
}
echo "== Run 1: remote install (fresh clone) as $INSTALL_USER =="
START=$(date +%s)
run_remote_install || fail "the remote install exited non-zero on a fresh clone"
echo " install took $(( $(date +%s) - START ))s"
assert_remote_install
echo "== Run 2: re-run over the existing clone (fetch + reset) =="
# The second bootstrap sees $CLONE_DIR/.git and updates it instead of cloning:
# the arm a user re-running curl|bash takes on every release after the first.
# Move the clone off the tag and drop its local tag ref first: a run that only
# had to notice the right commit was already checked out would pass even if
# the fetch and reset were both no-ops.
AWAY_SHA="$(su - "$INSTALL_USER" -c "cd '$CLONE_DIR' && \
GIT_AUTHOR_NAME=gate GIT_AUTHOR_EMAIL=gate@local \
GIT_COMMITTER_NAME=gate GIT_COMMITTER_EMAIL=gate@local \
git commit-tree 'HEAD^{tree}' -p HEAD -m moved")"
su - "$INSTALL_USER" -c "git -C '$CLONE_DIR' reset --hard '$AWAY_SHA' \
&& git -C '$CLONE_DIR' update-ref -d 'refs/tags/$GATE_TAG'" \
|| fail "could not move the clone off $GATE_TAG for the re-run arm"
[ "$(git -C "$CLONE_DIR" rev-parse HEAD)" = "$AWAY_SHA" ] \
|| fail "the clone did not move off $GATE_TAG; the re-run proves nothing"
[ -z "$(git -C "$CLONE_DIR" tag -l "$GATE_TAG")" ] \
|| fail "the clone kept refs/tags/$GATE_TAG; the re-run proves nothing"
run_remote_install || fail "the remote install exited non-zero over the existing clone"
grep -l "Updating existing clone" "$INSTALL_HOME"/.local/state/vocalinux/install-*.log >/dev/null 2>&1 \
|| fail "no install log shows 'Updating existing clone'; the fetch arm did not run"
assert_remote_install
echo "PASS: the remote install path (bootstrap -> tag -> clone/fetch -> handoff) installs ${NAME:-this distro}'s app"
+153
View File
@@ -230,3 +230,156 @@ def test_the_smoke_checks_what_an_import_walk_cannot_see():
"the smoke imports keyboard_backends without asserting a backend is"
" available, which is the failure the suse compiler packages fix"
)
REMOTE_GATE_SH = REPO_ROOT / "scripts" / "install-remote-test.sh"
def _remote_gate_text() -> str:
return REMOTE_GATE_SH.read_text(encoding="utf-8")
def _remote_bootstrap_case() -> str:
"""The remote gate's bootstrap `case "$ID"`, which is not the only case."""
blocks = re.findall(r'^case "\$ID" in\n(.*?)^esac', _remote_gate_text(), re.M | re.S)
bootstrap = [block for block in blocks if "apt-get" in block]
assert len(bootstrap) == 1, 'no single `case "$ID"` installs the bootstrap packages'
return bootstrap[0]
def _remote_job_image() -> str:
"""The container image the install-remote job runs the gate in."""
block = _job_block(_matrix_text(), "install-remote")
run = re.search(r'^\s*(\S+)\s*\\\n\s*bash "\$PWD/scripts/install-remote-test\.sh"', block, re.M)
assert run, "the install-remote job has no docker image line above the gate call"
return run.group(1)
def test_the_workflow_runs_the_remote_gate():
"""The remote path (bootstrap outside a checkout -> tag -> clone/fetch ->
handoff) is the one a curl|bash user takes; a gate that only runs the local
mode leaves it unguarded."""
block = _job_block(_matrix_text(), "install-remote")
assert re.search(r"^\s*docker run", block, re.M), "the remote job must run a container"
assert re.search(
r'^\s*bash "\$PWD/scripts/install-remote-test\.sh"$', block, re.M
), "the remote job no longer runs the remote gate script"
def test_the_remote_gate_can_fail_a_build():
"""Same rule as the local matrix: advisory jobs are documentation."""
block = _job_block(_matrix_text(), "install-remote")
assert not re.search(r"^\s*continue-on-error:\s*true", block, re.M), (
"the remote install gate is advisory; a gate that cannot fail a build"
" is documentation, not a gate"
)
def test_the_remote_gate_selects_a_tag():
"""Tag selection is half the remote contract; a run without --tag resolves
whatever the latest release API returns instead of a chosen ref."""
gate = _without_comments(_remote_gate_text())
assert re.search(r"--tag=", gate), (
"the remote gate no longer passes --tag; it tests the latest-release"
" fallback rather than the selectable-tag path"
)
def test_the_remote_gate_fails_closed_on_the_selected_tag():
"""Guardrail: installer, sourced modules and requirement exports must all
come from the selected tag. The only evidence is the clone's HEAD equalling
the tag commit the gate published."""
gate = _without_comments(_remote_gate_text())
assert re.search(r"rev-parse HEAD", gate)
assert re.search(r'"\$cloned_sha" = "\$EXPECTED_SHA"', gate), (
"the remote gate no longer compares the clone's HEAD to the tag commit;"
" installing a different revision would pass"
)
assert re.search(
r'rev-parse "\$GATE_TAG\^\{commit\}"', gate
), "the remote gate no longer checks the selected tag ref reached the clone"
def test_the_remote_gate_bootstraps_outside_a_checkout():
"""install.sh chooses local mode from a pyproject.toml in the working
directory; the remote path only runs when the bootstrap starts where no
checkout exists. Overriding the clone source is what keeps the run on this
commit instead of the public repo's last release."""
gate = _without_comments(_remote_gate_text())
assert "VOCALINUX_REPO_URL" in gate, (
"the remote gate no longer redirects the clone source; it would"
" install the public repo's tag rather than this commit"
)
installer = _without_comments(INSTALLER.read_text(encoding="utf-8"))
assert re.search(r"VOCALINUX_REPO_URL:-https://github\.com/VocaHQ/vocalinux\.git", installer), (
"install.sh no longer honors VOCALINUX_REPO_URL; the gate's mirror"
" cannot reach it and the clone falls back to the public repo"
)
def test_the_remote_gate_runs_as_an_unprivileged_user():
"""install.sh refuses to run as root and calls sudo directly. The remote
handoff re-execs as the same user, so a root run would test a path no
user takes twice over."""
gate = _without_comments(_remote_gate_text())
assert "useradd" in gate
assert re.search(r'su - "\$INSTALL_USER"', gate)
assert "NOPASSWD" in gate
def test_the_remote_gate_covers_the_fetch_arm():
"""The remote path has two ways in: `git clone` on a fresh machine and
`git fetch origin tag` + `git reset --hard` over an existing clone, which
is what every re-run hits. Asserting the update message in an install log
proves the second arm ran."""
gate = _without_comments(_remote_gate_text())
assert re.search(
r'grep -l "Updating existing clone"', gate
), "the remote gate no longer checks the fetch+reset arm ran"
assert gate.count("run_remote_install") >= 3, "the remote gate no longer runs the install twice"
def test_the_remote_gate_checks_handoff_artifacts():
"""handoff_to_tagged_installer appends --venv-dir and re-execs with the
remote marker; the remote venv and the relocated activation helper are the
only observable proof both arrived."""
gate = _without_comments(_remote_gate_text())
for construct in (
r'\[ -x "\$REMOTE_VENV/bin/python" \]',
r'\[ -f "\$INSTALL_HOME/\.local/bin/activate-vocalinux\.sh" \]',
):
assert re.search(construct, gate), f"the remote gate no longer asserts {construct}"
def test_the_remote_gate_smoke_tests_the_installed_app():
"""Same standard as the local gate: the app binary answers --version
through the wrapper the user's PATH carries."""
gate = _without_comments(_remote_gate_text())
assert re.search(r"'\$INSTALL_HOME/\.local/bin/vocalinux' --version", gate)
assert re.search(
r'\[ -x "\$INSTALL_HOME/\.local/bin/vocalinux" \]', gate
), "the remote gate no longer asserts the user-facing wrapper exists"
def test_the_remote_gate_has_a_bootstrap_arm_for_its_distro():
"""The job is pinned to one container; the gate's bootstrap arms are per
/etc/os-release ID. A pinned distro with no arm fails at the case
fallthrough 40 minutes into CI rather than here."""
image = _remote_job_image()
assert image in CONTAINER_OS_IDS, (
f"{image} is the remote gate's container but not in CONTAINER_OS_IDS;"
" add its /etc/os-release ID so this guard can check the gate handles it"
)
os_id = CONTAINER_OS_IDS[image]
bootstrap = _remote_bootstrap_case()
assert re.search(rf"^ [\w |-]*\b{re.escape(os_id)}\b[\w |-]*\)", bootstrap, re.M), (
f"{image} reports ID={os_id}, which scripts/install-remote-test.sh has"
" no bootstrap arm for"
)
def test_the_remote_gate_is_runnable_locally():
"""CI and the local entry point must run the same script."""
justfile = JUSTFILE.read_text(encoding="utf-8")
assert re.search(r"^remote-install-gate distro=", justfile, re.M)
assert "scripts/install-remote-test.sh" in justfile