Fix the review findings in connect-tailscale

tailscaled is waited for before tailscale up, a tailscaled that already runs on
the runner is not taken over and down leaves a daemon it did not start, sudo is
used only when the job is not root, an exact version is required to install
(with an optional sha256 of the archive), and the guide states when the node is
ephemeral and calls @main until the release.

Refs #185
Task: MIL-012#4
This commit is contained in:
2026-10-19 12:01:06 +08:00
parent 9e1cfa5fa8
commit 7a30560a74
4 changed files with 142 additions and 34 deletions
+49 -2
View File
@@ -36,6 +36,11 @@ case "$1" in
done
exit "${UP_EXIT:-0}" ;;
ping) exit "${PING_EXIT:-0}" ;;
version)
# the daemon answers only after tailscaled was started, and after DAEMON_DELAY calls
[ -f "$STUB_DAEMON" ] || exit 1
n=$(( $(cat "$STUB_DAEMON") + 1 )); echo "$n" > "$STUB_DAEMON"
[ "$n" -gt "${DAEMON_DELAY:-0}" ] ;;
esac
"""
@@ -52,13 +57,19 @@ def make_stub(path: Path, body: str) -> None:
def run(tmp_path: Path, command: str, **env: str) -> subprocess.CompletedProcess:
make_stub(tmp_path / "tailscale", TAILSCALE_STUB)
make_stub(tmp_path / "tailscaled", "#!/usr/bin/env bash\necho \"$*\" >> \"$STUB_LOG\"\n")
make_stub(
tmp_path / "tailscaled",
'#!/usr/bin/env bash\necho "$*" >> "$STUB_LOG"\necho 0 > "$STUB_DAEMON"\n',
)
full = {
"PATH": f"{tmp_path}:{os.environ['PATH']}",
"TS_SUDO": "",
"TS_STATE_DIR": str(tmp_path / "state"),
"STUB_LOG": str(tmp_path / "log"),
"STUB_KEY": str(tmp_path / "key"),
"STUB_DAEMON": str(tmp_path / "daemon"),
"TS_WAIT_INTERVAL": "0.05",
"TS_START_WAIT": "100",
**env,
}
return subprocess.run(
@@ -77,7 +88,7 @@ class TestDefinition:
def test_inputs_are_the_contract(self, document):
assert set(document["inputs"]) == {
"action", "authkey", "oauth-secret", "tags", "hostname", "version",
"userspace-networking", "args", "timeout", "health-check-host",
"sha256", "userspace-networking", "args", "timeout", "health-check-host",
}
def test_uses_gitea_contexts_only(self):
@@ -124,6 +135,8 @@ class TestUp:
({"TS_AUTHKEY": AUTHKEY, "TS_TAGS": "ci"}, "tags must look like"),
({"TS_AUTHKEY": AUTHKEY, "TS_TIMEOUT": "soon"}, "timeout"),
({"TS_AUTHKEY": AUTHKEY, "TS_VERSION": "1.x"}, "version"),
({"TS_AUTHKEY": AUTHKEY, "TS_VERSION": "latest"}, "version"),
({"TS_AUTHKEY": AUTHKEY, "TS_SHA256": "abc"}, "sha256"),
({"TS_AUTHKEY": AUTHKEY, "TS_USERSPACE": "yes"}, "userspace-networking"),
({"TS_AUTHKEY": AUTHKEY, "TS_HOSTNAME": "bad name"}, "hostname"),
],
@@ -148,6 +161,40 @@ class TestUp:
assert "health-check-host" in result.stdout
class TestDaemon:
def test_waits_until_the_daemon_answers(self, tmp_path):
result = run(tmp_path, "up", TS_AUTHKEY=AUTHKEY, DAEMON_DELAY="5")
assert result.returncode == 0, result.stdout
assert "up --auth-key" in log(tmp_path)
def test_a_daemon_that_never_answers_fails_before_up(self, tmp_path):
result = run(tmp_path, "up", TS_AUTHKEY=AUTHKEY, DAEMON_DELAY="1000", TS_START_WAIT="3")
assert result.returncode == 1
assert "::error::[connection] tailscaled did not answer" in result.stdout
assert "up --auth-key" not in log(tmp_path)
def test_a_daemon_of_the_host_is_not_taken_over(self, tmp_path):
(tmp_path / "daemon").write_text("0", encoding="utf-8")
result = run(tmp_path, "up", TS_AUTHKEY=AUTHKEY)
assert result.returncode == 1
assert "::error::[environment] a tailscaled already runs" in result.stdout
calls = log(tmp_path) # only the probe ran: no second daemon, no up
assert "--state=mem:" not in calls
assert "up --auth-key" not in calls
def test_down_leaves_a_daemon_it_did_not_start(self, tmp_path):
(tmp_path / "daemon").write_text("0", encoding="utf-8")
result = run(tmp_path, "down")
assert result.returncode == 0
assert "nothing to do" in result.stdout
assert not (tmp_path / "log").exists()
def test_without_tailscale_a_version_is_required(self, tmp_path):
result = run(tmp_path, "up", TS_AUTHKEY=AUTHKEY, TS_TAILSCALE="no-such-tailscale")
assert result.returncode == 1
assert "::error::[configuration] tailscale is not on the runner; set version" in result.stdout
class TestDownAndMask:
def test_down_logs_out_and_removes_the_state(self, tmp_path):
run(tmp_path, "up", TS_AUTHKEY=AUTHKEY)