diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md index 15e78033b..c17066a83 100644 --- a/docs/LOGBOOK.md +++ b/docs/LOGBOOK.md @@ -48956,17 +48956,19 @@ production browser smoke: **下一個 P0**: - 使用既有 Gitea `workflow_dispatch` / controlled CD lane 對含 `6a933e3f0` source 變更的最新 `main` 觸發 CD 與 code-review;CD 成功後再做 production GET/POST readback 與 `/zh-TW/iwooos` desktop / mobile smoke。 -## 2026-06-28 — 20:43 non-110 user runner 註冊前置實作與 188 prepare-only readback +## 2026-06-28 — 20:57 non-110 safe registration helper 與 unmanaged runner cleanup **完成內容**: -- 接續 `5f20d654d` non-110 user runner installer,補上安全 registration helper;installer 的 `REGISTER_NOW=1` 也改成只從互動 TTY 隱藏讀 token,透過 stdin 餵給 `act_runner register`,不使用 `--token` argv、不列印 token、不讀 `.runner` 內容。 -- 188 已套用 user-level Docker runner prepare-only:`/home/ollama/awoooi-non110-runner/data/config.yaml`、user systemd service、rollback service 與 `gitea/act_runner:latest` image 都可讀回;service 維持 `loaded/inactive/disabled`。 -- 舊 `/home/ollama/act-runner-awoooi` 裸 binary scaffold 確認沒有 `.runner` 後移到 disabled quarantine,避免 verifier 同時看見兩套 runner source。 +- 接續 host-binary user service apply gate,補上 `ops/runner/register-awoooi-non110-runner.sh`;helper 預設對準 `/home/ollama/act-runner-awoooi/act_runner` 與同目錄 `config.yaml` / `.runner`。 +- safe registration helper 只允許互動 TTY 隱藏讀 token,透過 stdin 餵給 `act_runner register`,不使用 `--token` argv、不列印 token、不讀 `.runner` 內容;Codex 不執行 token entry path。 +- `ops/runner/check-awoooi-non110-runner-readiness.sh` 補上 unmanaged runner container guard:random-name `gitea/act_runner` 會被 blocker 擋下,明確命名的既有 StockPlatform runner 不列入 AWOOI non-110 blocker。 +- 188 已停掉兩個 random-name runner container;未刪匿名 volume、未讀內容。退休 Docker runner dir `/home/ollama/awoooi-non110-runner` 確認沒有 `.runner` 後移到 disabled quarantine。 **驗證結果**: -- 本地:`bash -n`、runner focused pytest `9 passed`、`guard-gitea-runner-pressure.py`、`git diff --check`。 -- 188 verifier:`READY_CONFIG_COUNT=1`、`READY_BINARY_COUNT=1`、`READY_SERVICE_COUNT=1`、`READY_REGISTRATION_COUNT=0`、`READY_ACTIVE_SERVICE_COUNT=0`、`BLOCKER_COUNT=3`,safe next step 指向 safe registration helper。 -- 110 readback:runner units 維持 masked/inactive,`ACTIVE_JOB_CONTAINERS=0`,failclosed enforcer timer active/enabled。 +- 本地:`bash -n`、runner focused pytest `11 passed`、Delivery Workbench focused test `1 passed`、`guard-gitea-runner-pressure.py`、`git diff --check`。 +- 188 final verifier:`READY_CONFIG_COUNT=1`、`READY_BINARY_COUNT=1`、`READY_SERVICE_COUNT=1`、`READY_REGISTRATION_COUNT=0`、`READY_ACTIVE_SERVICE_COUNT=0`、`UNMANAGED_RUNNER_CONTAINERS=none`、`BLOCKER_COUNT=3`,safe next step 指向 safe registration helper。 +- 188 只剩明確屬於 StockPlatform 的命名 act_runner container;`ACTIVE_ACTION_CONTAINERS=0`。 +- 110 readback:runner units 維持 masked/inactive,`ACTIVE_JOB_CONTAINERS=0`。 **仍維持**: - 沒有讀、複製、貼上、外送 runner token / secret / `.env` / raw sessions / SQLite / auth。 diff --git a/ops/runner/check-awoooi-non110-runner-readiness.sh b/ops/runner/check-awoooi-non110-runner-readiness.sh index e286e8ada..1a039157d 100755 --- a/ops/runner/check-awoooi-non110-runner-readiness.sh +++ b/ops/runner/check-awoooi-non110-runner-readiness.sh @@ -14,6 +14,7 @@ RUNNER_BINARY_PATHS="${RUNNER_BINARY_PATHS:-${RUNNER_HOME}/awoooi-non110-runner/ RUNNER_DOCKER_IMAGES="${RUNNER_DOCKER_IMAGES:-gitea/act_runner:latest}" RUNNER_REGISTRATION_PATHS="${RUNNER_REGISTRATION_PATHS:-${RUNNER_HOME}/awoooi-non110-runner/data/.runner ${RUNNER_HOME}/awoooi-non110-runner/.runner ${RUNNER_HOME}/act-runner-awoooi/.runner /home/wooo/act-runner-awoooi/.runner /home/wooo/awoooi-act-runner/.runner /home/wooo/awoooi-non110-runner/.runner /home/wooo/act-runner/.runner}" RUNNER_SERVICE_NAMES="${RUNNER_SERVICE_NAMES:-awoooi-non110-runner.service gitea-act-runner-awoooi.service gitea-act-runner-host.service}" +ALLOWED_RUNNER_CONTAINER_NAMES="${ALLOWED_RUNNER_CONTAINER_NAMES:-awoooi-non110-runner stockplatform-ubuntu-runner}" ALLOWED_LABEL_NAMES="${ALLOWED_LABEL_NAMES:-awoooi-non110-host awoooi-non110-ubuntu awoooi-host awoooi-ubuntu}" FORBIDDEN_LABEL_RE="${FORBIDDEN_LABEL_RE:-^(ubuntu-latest|ubuntu-[0-9].*|self-hosted|stockplatform.*|stock-platform.*|headless.*|playwright.*)$}" MAX_CAPACITY="${MAX_CAPACITY:-1}" @@ -155,6 +156,31 @@ active_action_container_count() { docker ps --format '{{.Names}}' 2>/dev/null | grep -Ec '^GITEA-ACTIONS-TASK-' || true } +allowed_runner_container_name() { + local name="$1" + local allowed + for allowed in $ALLOWED_RUNNER_CONTAINER_NAMES; do + [ "$name" = "$allowed" ] && return 0 + done + return 1 +} + +unmanaged_runner_containers() { + if ! command_exists docker; then + return 0 + fi + docker ps --format '{{.Names}}|{{.Image}}' 2>/dev/null | while IFS='|' read -r name image; do + [ -n "$name" ] || continue + case "$image" in + gitea/act_runner:*|*/gitea/act_runner:*) + if ! allowed_runner_container_name "$name"; then + printf '%s\n' "$name" + fi + ;; + esac + done +} + heavy_process_count() { local count=0 count="$( @@ -401,12 +427,15 @@ check_rollback() { check_pressure() { section "pressure readback" - local containers heavy + local containers heavy unmanaged containers="$(active_action_container_count)" heavy="$(heavy_process_count)" + unmanaged="$(unmanaged_runner_containers | paste -sd, -)" printf 'ACTIVE_ACTION_CONTAINERS=%s\n' "$containers" + printf 'UNMANAGED_RUNNER_CONTAINERS=%s\n' "${unmanaged:-none}" printf 'HEAVY_PROCESS_COUNT=%s max=%s\n' "$heavy" "$MAX_HEAVY_PROCESS_COUNT" [ "$containers" = "0" ] || blocker "active_action_containers_present:${containers}" + [ -z "$unmanaged" ] || blocker "unmanaged_runner_containers_present:${unmanaged}" [ "$heavy" -le "$MAX_HEAVY_PROCESS_COUNT" ] || blocker "heavy_processes_present:${heavy}" load_per_core_ok || blocker "host_load_per_core_too_high" } diff --git a/ops/runner/register-awoooi-non110-runner.sh b/ops/runner/register-awoooi-non110-runner.sh index 8724bc8a7..7b1616b2b 100755 --- a/ops/runner/register-awoooi-non110-runner.sh +++ b/ops/runner/register-awoooi-non110-runner.sh @@ -9,11 +9,10 @@ set -euo pipefail GITEA_INSTANCE="${GITEA_INSTANCE:-http://192.168.0.110:3001}" RUNNER_NAME="${RUNNER_NAME:-awoooi-non110-188}" RUNNER_LABELS="${RUNNER_LABELS:-awoooi-non110-host:host,awoooi-non110-ubuntu:docker://192.168.0.110:5000/awoooi/ci-runner:act-22.04}" -RUNNER_DIR="${RUNNER_DIR:-${HOME}/awoooi-non110-runner}" -DATA_DIR="${DATA_DIR:-${RUNNER_DIR}/data}" -ACT_RUNNER_IMAGE="${ACT_RUNNER_IMAGE:-gitea/act_runner:latest}" -RUNNER_CONFIG="${RUNNER_CONFIG:-${DATA_DIR}/config.yaml}" -RUNNER_REGISTRATION="${RUNNER_REGISTRATION:-${DATA_DIR}/.runner}" +RUNNER_DIR="${RUNNER_DIR:-${HOME}/act-runner-awoooi}" +RUNNER_BINARY="${RUNNER_BINARY:-${RUNNER_DIR}/act_runner}" +RUNNER_CONFIG="${RUNNER_CONFIG:-${RUNNER_DIR}/config.yaml}" +RUNNER_REGISTRATION="${RUNNER_REGISTRATION:-${RUNNER_DIR}/.runner}" usage() { cat <<'EOF' @@ -22,8 +21,8 @@ Usage: ops/runner/register-awoooi-non110-runner.sh Environment overrides: - GITEA_INSTANCE, RUNNER_NAME, RUNNER_LABELS, RUNNER_DIR, DATA_DIR, - ACT_RUNNER_IMAGE, RUNNER_CONFIG, RUNNER_REGISTRATION + GITEA_INSTANCE, RUNNER_NAME, RUNNER_LABELS, RUNNER_DIR, RUNNER_BINARY, + RUNNER_CONFIG, RUNNER_REGISTRATION Safety contract: - requires an interactive TTY for token entry @@ -52,18 +51,15 @@ metadata() { log "gitea_instance=${GITEA_INSTANCE}" log "runner_name=${RUNNER_NAME}" log "runner_dir=${RUNNER_DIR}" - log "data_dir=${DATA_DIR}" - log "act_runner_image=${ACT_RUNNER_IMAGE}" + log "runner_binary=${RUNNER_BINARY}" log "runner_config=${RUNNER_CONFIG}" log "runner_registration=${RUNNER_REGISTRATION}" } preflight() { - command -v docker >/dev/null 2>&1 || die "docker_missing" [ -d "$RUNNER_DIR" ] || die "runner_dir_missing:${RUNNER_DIR}" - [ -d "$DATA_DIR" ] || die "data_dir_missing:${DATA_DIR}" + [ -x "$RUNNER_BINARY" ] || die "runner_binary_missing_or_not_executable:${RUNNER_BINARY}" [ -r "$RUNNER_CONFIG" ] || die "runner_config_missing_or_not_readable:${RUNNER_CONFIG}" - docker image inspect "$ACT_RUNNER_IMAGE" >/dev/null 2>&1 || die "act_runner_image_missing:${ACT_RUNNER_IMAGE}" if [ -s "$RUNNER_REGISTRATION" ] && [ "${ALLOW_REREGISTER:-0}" != "1" ]; then log "runner_registration_present=1" @@ -97,14 +93,11 @@ read_token_from_tty() { register_runner() { cd "$RUNNER_DIR" set +x - printf '%s\n' "$RUNNER_TOKEN" | docker run --rm -i \ - -v "${DATA_DIR}:/data" \ - -e CONFIG_FILE=/data/config.yaml \ - "$ACT_RUNNER_IMAGE" register \ + printf '%s\n' "$RUNNER_TOKEN" | "$RUNNER_BINARY" register \ --instance "$GITEA_INSTANCE" \ --name "$RUNNER_NAME" \ --labels "$RUNNER_LABELS" \ - --config /data/config.yaml + --config "$RUNNER_CONFIG" unset RUNNER_TOKEN } diff --git a/ops/runner/test_check_awoooi_non110_runner_readiness.py b/ops/runner/test_check_awoooi_non110_runner_readiness.py index cbdc1128b..2e07c07ad 100644 --- a/ops/runner/test_check_awoooi_non110_runner_readiness.py +++ b/ops/runner/test_check_awoooi_non110_runner_readiness.py @@ -61,6 +61,7 @@ def _run_verifier( *, active_service: bool = True, unit_target_matches: bool = True, + unmanaged_runner_container: bool = False, ) -> subprocess.CompletedProcess[str]: fake_bin = tmp_path / "bin" unit_dir = tmp_path / "units" @@ -97,11 +98,22 @@ esac exit 1 """, ) + unmanaged_runner_output = ( + "printf 'condescending_wilbur|gitea/act_runner:latest\\n'" + if unmanaged_runner_container + else "exit 0" + ) _write_fake_bin( fake_bin, "docker", - """#!/usr/bin/env bash -if [ "${1:-}" = "ps" ]; then exit 0; fi + f"""#!/usr/bin/env bash +if [ "${{1:-}}" = "ps" ]; then + if printf '%s\\n' "$*" | grep -q 'Image'; then + {unmanaged_runner_output} + exit 0 + fi + exit 0 +fi exit 0 """, ) @@ -202,3 +214,20 @@ def test_non110_readiness_blocks_service_target_mismatch(tmp_path: Path) -> None assert "BLOCKER runner_service_target_mismatch:awoooi-non110-runner.service" in result.stdout assert "RUNNER_SERVICE_TARGETS unit=awoooi-non110-runner.service" in result.stdout assert "AWOOOI_NON110_RUNNER_READY=0" in result.stdout + + +def test_non110_readiness_blocks_unmanaged_act_runner_container(tmp_path: Path) -> None: + registration_path = tmp_path / ".runner" + registration_path.write_text("secret-token-like-content-not-printed\n", encoding="utf-8") + result = _run_verifier( + tmp_path, + registration_path, + unmanaged_runner_container=True, + ) + assert result.returncode == 1 + assert "UNMANAGED_RUNNER_CONTAINERS=condescending_wilbur" in result.stdout + assert ( + "BLOCKER unmanaged_runner_containers_present:condescending_wilbur" + in result.stdout + ) + assert "secret-token-like-content" not in result.stdout diff --git a/ops/runner/test_register_awoooi_non110_runner.py b/ops/runner/test_register_awoooi_non110_runner.py index e2455108a..c4027103e 100644 --- a/ops/runner/test_register_awoooi_non110_runner.py +++ b/ops/runner/test_register_awoooi_non110_runner.py @@ -8,7 +8,8 @@ from pathlib import Path ROOT = Path(__file__).resolve().parents[2] REGISTER = ROOT / "ops/runner/register-awoooi-non110-runner.sh" -INSTALLER = ROOT / "ops/runner/install-awoooi-non110-user-runner.sh" +COMPAT_WRAPPER = ROOT / "ops/runner/install-awoooi-non110-user-runner.sh" +USER_SERVICE_INSTALLER = ROOT / "ops/runner/install-awoooi-non110-runner-user-service.sh" def test_register_helper_has_no_token_argv_path() -> None: @@ -20,28 +21,21 @@ def test_register_helper_has_no_token_argv_path() -> None: def test_user_installer_has_no_token_argv_path() -> None: - text = INSTALLER.read_text(encoding="utf-8") - assert "--token" not in text - assert "printf '%s\\n' \"$RUNNER_TOKEN\" | docker run --rm -i" in text - assert "runner_registration_requires_external_credentialed_channel" not in text + wrapper = COMPAT_WRAPPER.read_text(encoding="utf-8") + installer = USER_SERVICE_INSTALLER.read_text(encoding="utf-8") + assert "--token" not in wrapper + assert "--token" not in installer + assert "retired_wrapper_does_not_register_or_start_runner" in wrapper + assert "never\n# registers a runner" in installer def test_register_helper_dry_run_requires_no_token(tmp_path: Path) -> None: runner_dir = tmp_path / "runner" - data_dir = runner_dir / "data" - data_dir.mkdir(parents=True) - fake_bin = tmp_path / "bin" - fake_bin.mkdir() - docker = fake_bin / "docker" - docker.write_text( - """#!/usr/bin/env bash -if [ "${1:-}" = "image" ] && [ "${2:-}" = "inspect" ]; then exit 0; fi -exit 99 -""", - encoding="utf-8", - ) - docker.chmod(0o755) - config = data_dir / "config.yaml" + runner_dir.mkdir(parents=True) + binary = runner_dir / "act_runner" + binary.write_text("#!/usr/bin/env bash\nexit 99\n", encoding="utf-8") + binary.chmod(0o755) + config = runner_dir / "config.yaml" config.write_text("runner:\n capacity: 1\n", encoding="utf-8") result = subprocess.run( @@ -50,11 +44,10 @@ exit 99 env={ **os.environ, "DRY_RUN": "1", - "PATH": f"{fake_bin}:{os.environ['PATH']}", "RUNNER_DIR": str(runner_dir), - "DATA_DIR": str(data_dir), + "RUNNER_BINARY": str(binary), "RUNNER_CONFIG": str(config), - "RUNNER_REGISTRATION": str(data_dir / ".runner"), + "RUNNER_REGISTRATION": str(runner_dir / ".runner"), }, text=True, stdout=subprocess.PIPE,