From 7cf24d1ee9fcfdb6b6f9a03dbb70f7321af1647f Mon Sep 17 00:00:00 2001 From: auric Date: Wed, 10 Jun 2026 16:51:07 +0800 Subject: [PATCH 1/4] Implement github-devloop ready state --- .github/workflows/pages.yml | 37 ++++++ README.md | 2 +- packages/site-board/core.lua | 77 +++++++++++++ .../departments/board_scan/main.lua | 1 + .../departments/probe_scan/main.lua | 56 +++++++++ .../probe_scan/probe_scan_test.lua | 107 ++++++++++++++++++ site/probe-manifest | 6 + 7 files changed, 285 insertions(+), 1 deletion(-) create mode 100644 packages/site-board/departments/probe_scan/main.lua create mode 100644 packages/site-board/departments/probe_scan/probe_scan_test.lua create mode 100644 site/probe-manifest diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 8fb70fd..992f66d 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -34,3 +34,40 @@ jobs: - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v4 + + - name: Live probe published site + run: | + set -euo pipefail + checked=0 + failures=0 + network_errors=0 + results=() + while IFS= read -r path || [ -n "$path" ]; do + path="${path%%#*}" + path="${path#"${path%%[![:space:]]*}"}" + path="${path%"${path##*[![:space:]]}"}" + [ -n "$path" ] || continue + checked=$((checked + 1)) + url="https://chronoaiproject.github.io/fkst-website${path}" + code="$(curl -sL -o /dev/null -w '%{http_code}' -- "$url")" || code="error" + [ -n "$code" ] || code="error" + results+=("${path}:${code}") + if [ "$code" = "200" ]; then + continue + elif [ "$code" = "error" ]; then + network_errors=$((network_errors + 1)) + else + failures=$((failures + 1)) + fi + done < site/probe-manifest + joined="$(IFS=,; echo "${results[*]}")" + if [ "$checked" -eq 0 ]; then + echo "fkst-website dept=pages tag=skip PROBE paths=0 reason=empty-manifest results=" + elif [ "$network_errors" -eq "$checked" ]; then + echo "fkst-website dept=pages tag=skip PROBE paths=$checked reason=all-paths-network-error results=$joined" + elif [ "$failures" -gt 0 ]; then + echo "fkst-website dept=pages tag=fail PROBE paths=$checked failures=$failures results=$joined" + exit 1 + else + echo "fkst-website dept=pages tag=ok PROBE paths=$checked results=$joined" + fi diff --git a/README.md b/README.md index c120b63..01f5e94 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ This repo is **English-primary, zh-en bilingual**: source files are English; ext ## 包 -- `packages/site-board/`(flat):站点数据源 v0。cron 轮询 `FKST_GITHUB_REPO` 的 open issue/PR 板面,构建 `fkst-website.board.v1` 快照 JSON;`FKST_SITE_WRITE=1` 且设置 `FKST_SITE_PUBLISH_ROOT` 时原子发布 `board.json`(写 tmp + mv),否则 dry-run 只记日志。 +- `packages/site-board/`(flat):站点数据源 v0。cron 轮询 `FKST_GITHUB_REPO` 的 open issue/PR 板面,构建 `fkst-website.board.v1` 快照 JSON;`FKST_SITE_WRITE=1` 且设置 `FKST_SITE_PUBLISH_ROOT` 时原子发布 `board.json`(写 tmp + mv),否则 dry-run 只记日志。`probe_scan` 读取 `site/probe-manifest` 对发布站点做只读 live probe,只输出可 grep 的 `PROBE` ok/fail/skip 日志。 ## 构建 / 测试 diff --git a/packages/site-board/core.lua b/packages/site-board/core.lua index 75dd85f..4bc33a6 100644 --- a/packages/site-board/core.lua +++ b/packages/site-board/core.lua @@ -1,5 +1,7 @@ local M = {} +M.SITE_BASE_URL = "https://chronoaiproject.github.io/fkst-website" + -- Env access goes through exec_sync so tests can mock it and unmocked reads -- fail closed in test mode. Only allowlisted names are readable. local ENV_ALLOWLIST = { @@ -53,6 +55,81 @@ function M.shell_single_quote(value) return "'" .. tostring(value):gsub("'", "'\\''") .. "'" end +local function trim(value) + return tostring(value):match("^%s*(.-)%s*$") +end + +function M.read_probe_manifest(path) + local handle, open_err = io.open(path, "r") + if handle == nil then + return nil, "manifest open failed: " .. tostring(open_err) + end + + local paths = {} + local seen = {} + for line in handle:lines() do + local item = trim(line:gsub("#.*$", "")) + if item ~= "" then + if item:sub(1, 1) ~= "/" or item:find("%s") ~= nil then + handle:close() + return nil, "invalid probe path: " .. item + end + if not seen[item] then + seen[item] = true + table.insert(paths, item) + end + end + end + handle:close() + + if #paths == 0 then + return nil, "manifest has no probe paths" + end + return paths +end + +function M.probe_url(path) + if type(path) ~= "string" or path:sub(1, 1) ~= "/" or path:find("%s") ~= nil then + error("invalid probe path: " .. tostring(path)) + end + return M.SITE_BASE_URL .. path +end + +function M.curl_probe_cmd(path) + return "curl -sL -o /dev/null -w '%{http_code}' -- " .. M.shell_single_quote(M.probe_url(path)) +end + +function M.classify_probe_result(result) + if type(result) ~= "table" or result.exit_code ~= 0 then + return "error" + end + local code = trim(result.stdout or "") + if code == "" then + return "error" + end + return code +end + +local function encode_probe_item(item) + return tostring(item.path) .. ":" .. tostring(item.code) +end + +function M.probe_log_fields(statuses, extra_fields) + local fields = { + "PROBE", + "paths=" .. tostring(#statuses), + } + for _, field in ipairs(extra_fields or {}) do + table.insert(fields, field) + end + local encoded = {} + for _, item in ipairs(statuses) do + table.insert(encoded, encode_probe_item(item)) + end + table.insert(fields, "results=" .. table.concat(encoded, ",")) + return fields +end + function M.is_valid_repo(repo) return type(repo) == "string" and repo:match("^[%w._-]+/[%w._-]+$") ~= nil end diff --git a/packages/site-board/departments/board_scan/main.lua b/packages/site-board/departments/board_scan/main.lua index fadd29f..0bd2a8c 100644 --- a/packages/site-board/departments/board_scan/main.lua +++ b/packages/site-board/departments/board_scan/main.lua @@ -4,6 +4,7 @@ local M = {} M.spec = { consumes = { "board_poll_tick" }, + fanout = { "board_poll_tick" }, stall_window = "30s", } diff --git a/packages/site-board/departments/probe_scan/main.lua b/packages/site-board/departments/probe_scan/main.lua new file mode 100644 index 0000000..08cc859 --- /dev/null +++ b/packages/site-board/departments/probe_scan/main.lua @@ -0,0 +1,56 @@ +local core = require("core") + +local M = {} + +M.spec = { + consumes = { "board_poll_tick" }, + fanout = { "board_poll_tick" }, + stall_window = "30s", +} + +local function manifest_path() + return "site/probe-manifest" +end + +local function probe_path(path) + local result = exec_sync({ cmd = core.curl_probe_cmd(path), timeout = 30 }) + return core.classify_probe_result(result) +end + +function pipeline(event) + local paths, manifest_err = core.read_probe_manifest(manifest_path()) + if paths == nil then + error("site probe manifest failed: " .. tostring(manifest_err)) + end + + local statuses = {} + local failures = {} + local network_errors = 0 + for _, path in ipairs(paths) do + local code = probe_path(path) + table.insert(statuses, { path = path, code = code }) + if code == "error" then + network_errors = network_errors + 1 + elseif code ~= "200" then + table.insert(failures, { path = path, code = code }) + end + end + + if network_errors == #statuses then + core.log_line("warn", "probe_scan", "skip", core.probe_log_fields(statuses, { + "reason=all-paths-network-error", + })) + return + end + + if #failures > 0 then + core.log_line("warn", "probe_scan", "fail", core.probe_log_fields(failures, { + "checked=" .. tostring(#statuses), + })) + return + end + + core.log_line("info", "probe_scan", "ok", core.probe_log_fields(statuses)) +end + +return M diff --git a/packages/site-board/departments/probe_scan/probe_scan_test.lua b/packages/site-board/departments/probe_scan/probe_scan_test.lua new file mode 100644 index 0000000..fdc8d03 --- /dev/null +++ b/packages/site-board/departments/probe_scan/probe_scan_test.lua @@ -0,0 +1,107 @@ +local t = fkst.test +local core = require("core") + +local function nonce() + return tostring({}):gsub("[^%w._-]", "_") +end + +local function runtime_root(name) + return "/tmp/fkst-website-test/site-board-probe/" .. tostring(now()) .. "/" .. nonce() .. "/" .. name +end + +local function opts(name) + return { + env = { + FKST_RUNTIME_ROOT = runtime_root(name), + }, + } +end + +local function run_probe(run_opts) + return t.run_department("departments/probe_scan/main.lua", { + queue = "board_poll_tick", + payload = {}, + }, run_opts) +end + +local function mock_probe(path, code) + t.mock_command(core.curl_probe_cmd(path), { stdout = code, exit_code = 0 }) +end + +local function mock_probe_error(path) + t.mock_command(core.curl_probe_cmd(path), { stdout = "", stderr = "network down", exit_code = 7 }) +end + +local function github_issue_calls() + local calls = {} + for _, call in ipairs(t.command_calls()) do + if call.rendered:find("gh issue", 1, true) ~= nil or call.rendered:find("github_issue_create_request", 1, true) ~= nil then + table.insert(calls, call) + end + end + return calls +end + +return { + test_manifest_is_canonical_page_list = function() + local paths = core.read_probe_manifest("site/probe-manifest") + t.eq(#paths, 6) + t.eq(paths[1], "/") + t.eq(paths[2], "/zh/") + t.eq(paths[3], "/architecture.html") + t.eq(paths[4], "/doctrine.html") + t.eq(paths[5], "/zh/architecture.html") + t.eq(paths[6], "/zh/doctrine.html") + end, + + test_curl_probe_cmd_targets_published_site = function() + t.eq( + core.curl_probe_cmd("/architecture.html"), + "curl -sL -o /dev/null -w '%{http_code}' -- 'https://chronoaiproject.github.io/fkst-website/architecture.html'" + ) + local ok = pcall(core.curl_probe_cmd, "architecture.html") + t.eq(ok, false) + end, + + test_probe_all_200_logs_ok_without_issue_request = function() + mock_probe("/", "200") + mock_probe("/zh/", "200") + mock_probe("/architecture.html", "200") + mock_probe("/doctrine.html", "200") + mock_probe("/zh/architecture.html", "200") + mock_probe("/zh/doctrine.html", "200") + + local result = run_probe(opts("all-ok")) + t.eq(result.exit_code, 0) + t.eq(#result.raises, 0) + t.eq(#github_issue_calls(), 0) + end, + + test_probe_one_404_logs_failure_without_issue_request = function() + mock_probe("/", "200") + mock_probe("/zh/", "200") + mock_probe("/architecture.html", "404") + mock_probe("/doctrine.html", "200") + mock_probe("/zh/architecture.html", "200") + mock_probe("/zh/doctrine.html", "200") + + local result = run_probe(opts("one-404")) + t.eq(result.exit_code, 0) + t.eq(#result.raises, 0) + t.eq(#github_issue_calls(), 0) + end, + + test_probe_all_network_error_logs_skip_without_issue_request = function() + mock_probe_error("/") + mock_probe_error("/zh/") + mock_probe_error("/architecture.html") + mock_probe_error("/doctrine.html") + mock_probe_error("/zh/architecture.html") + mock_probe_error("/zh/doctrine.html") + + local result = run_probe(opts("all-network-error")) + t.eq(result.exit_code, 0) + t.eq(#result.raises, 0) + t.eq(#github_issue_calls(), 0) + end, +} diff --git a/site/probe-manifest b/site/probe-manifest new file mode 100644 index 0000000..fa9ae07 --- /dev/null +++ b/site/probe-manifest @@ -0,0 +1,6 @@ +/ +/zh/ +/architecture.html +/doctrine.html +/zh/architecture.html +/zh/doctrine.html From 5ed768d4425f9774f52613a85b37c4ca001279c1 Mon Sep 17 00:00:00 2001 From: auric Date: Wed, 10 Jun 2026 17:02:41 +0800 Subject: [PATCH 2/4] Fix github-devloop review feedback --- .github/workflows/pages.yml | 4 +- README.md | 2 +- packages/site-board/core.lua | 77 ------------- .../departments/board_scan/main.lua | 1 - .../departments/probe_scan/main.lua | 56 --------- .../probe_scan/probe_scan_test.lua | 107 ------------------ 6 files changed, 3 insertions(+), 244 deletions(-) delete mode 100644 packages/site-board/departments/probe_scan/main.lua delete mode 100644 packages/site-board/departments/probe_scan/probe_scan_test.lua diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 992f66d..6f654df 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -65,8 +65,8 @@ jobs: echo "fkst-website dept=pages tag=skip PROBE paths=0 reason=empty-manifest results=" elif [ "$network_errors" -eq "$checked" ]; then echo "fkst-website dept=pages tag=skip PROBE paths=$checked reason=all-paths-network-error results=$joined" - elif [ "$failures" -gt 0 ]; then - echo "fkst-website dept=pages tag=fail PROBE paths=$checked failures=$failures results=$joined" + elif [ "$failures" -gt 0 ] || [ "$network_errors" -gt 0 ]; then + echo "fkst-website dept=pages tag=fail PROBE paths=$checked failures=$failures network_errors=$network_errors results=$joined" exit 1 else echo "fkst-website dept=pages tag=ok PROBE paths=$checked results=$joined" diff --git a/README.md b/README.md index 01f5e94..22da778 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ This repo is **English-primary, zh-en bilingual**: source files are English; ext ## 包 -- `packages/site-board/`(flat):站点数据源 v0。cron 轮询 `FKST_GITHUB_REPO` 的 open issue/PR 板面,构建 `fkst-website.board.v1` 快照 JSON;`FKST_SITE_WRITE=1` 且设置 `FKST_SITE_PUBLISH_ROOT` 时原子发布 `board.json`(写 tmp + mv),否则 dry-run 只记日志。`probe_scan` 读取 `site/probe-manifest` 对发布站点做只读 live probe,只输出可 grep 的 `PROBE` ok/fail/skip 日志。 +- `packages/site-board/`(flat):站点数据源 v0。cron 轮询 `FKST_GITHUB_REPO` 的 open issue/PR 板面,构建 `fkst-website.board.v1` 快照 JSON;`FKST_SITE_WRITE=1` 且设置 `FKST_SITE_PUBLISH_ROOT` 时原子发布 `board.json`(写 tmp + mv),否则 dry-run 只记日志。GitHub Pages deploy 后读取 `site/probe-manifest` 对发布站点做只读 live probe,只输出可 grep 的 `PROBE` ok/fail/skip 日志。 ## 构建 / 测试 diff --git a/packages/site-board/core.lua b/packages/site-board/core.lua index 4bc33a6..75dd85f 100644 --- a/packages/site-board/core.lua +++ b/packages/site-board/core.lua @@ -1,7 +1,5 @@ local M = {} -M.SITE_BASE_URL = "https://chronoaiproject.github.io/fkst-website" - -- Env access goes through exec_sync so tests can mock it and unmocked reads -- fail closed in test mode. Only allowlisted names are readable. local ENV_ALLOWLIST = { @@ -55,81 +53,6 @@ function M.shell_single_quote(value) return "'" .. tostring(value):gsub("'", "'\\''") .. "'" end -local function trim(value) - return tostring(value):match("^%s*(.-)%s*$") -end - -function M.read_probe_manifest(path) - local handle, open_err = io.open(path, "r") - if handle == nil then - return nil, "manifest open failed: " .. tostring(open_err) - end - - local paths = {} - local seen = {} - for line in handle:lines() do - local item = trim(line:gsub("#.*$", "")) - if item ~= "" then - if item:sub(1, 1) ~= "/" or item:find("%s") ~= nil then - handle:close() - return nil, "invalid probe path: " .. item - end - if not seen[item] then - seen[item] = true - table.insert(paths, item) - end - end - end - handle:close() - - if #paths == 0 then - return nil, "manifest has no probe paths" - end - return paths -end - -function M.probe_url(path) - if type(path) ~= "string" or path:sub(1, 1) ~= "/" or path:find("%s") ~= nil then - error("invalid probe path: " .. tostring(path)) - end - return M.SITE_BASE_URL .. path -end - -function M.curl_probe_cmd(path) - return "curl -sL -o /dev/null -w '%{http_code}' -- " .. M.shell_single_quote(M.probe_url(path)) -end - -function M.classify_probe_result(result) - if type(result) ~= "table" or result.exit_code ~= 0 then - return "error" - end - local code = trim(result.stdout or "") - if code == "" then - return "error" - end - return code -end - -local function encode_probe_item(item) - return tostring(item.path) .. ":" .. tostring(item.code) -end - -function M.probe_log_fields(statuses, extra_fields) - local fields = { - "PROBE", - "paths=" .. tostring(#statuses), - } - for _, field in ipairs(extra_fields or {}) do - table.insert(fields, field) - end - local encoded = {} - for _, item in ipairs(statuses) do - table.insert(encoded, encode_probe_item(item)) - end - table.insert(fields, "results=" .. table.concat(encoded, ",")) - return fields -end - function M.is_valid_repo(repo) return type(repo) == "string" and repo:match("^[%w._-]+/[%w._-]+$") ~= nil end diff --git a/packages/site-board/departments/board_scan/main.lua b/packages/site-board/departments/board_scan/main.lua index 0bd2a8c..fadd29f 100644 --- a/packages/site-board/departments/board_scan/main.lua +++ b/packages/site-board/departments/board_scan/main.lua @@ -4,7 +4,6 @@ local M = {} M.spec = { consumes = { "board_poll_tick" }, - fanout = { "board_poll_tick" }, stall_window = "30s", } diff --git a/packages/site-board/departments/probe_scan/main.lua b/packages/site-board/departments/probe_scan/main.lua deleted file mode 100644 index 08cc859..0000000 --- a/packages/site-board/departments/probe_scan/main.lua +++ /dev/null @@ -1,56 +0,0 @@ -local core = require("core") - -local M = {} - -M.spec = { - consumes = { "board_poll_tick" }, - fanout = { "board_poll_tick" }, - stall_window = "30s", -} - -local function manifest_path() - return "site/probe-manifest" -end - -local function probe_path(path) - local result = exec_sync({ cmd = core.curl_probe_cmd(path), timeout = 30 }) - return core.classify_probe_result(result) -end - -function pipeline(event) - local paths, manifest_err = core.read_probe_manifest(manifest_path()) - if paths == nil then - error("site probe manifest failed: " .. tostring(manifest_err)) - end - - local statuses = {} - local failures = {} - local network_errors = 0 - for _, path in ipairs(paths) do - local code = probe_path(path) - table.insert(statuses, { path = path, code = code }) - if code == "error" then - network_errors = network_errors + 1 - elseif code ~= "200" then - table.insert(failures, { path = path, code = code }) - end - end - - if network_errors == #statuses then - core.log_line("warn", "probe_scan", "skip", core.probe_log_fields(statuses, { - "reason=all-paths-network-error", - })) - return - end - - if #failures > 0 then - core.log_line("warn", "probe_scan", "fail", core.probe_log_fields(failures, { - "checked=" .. tostring(#statuses), - })) - return - end - - core.log_line("info", "probe_scan", "ok", core.probe_log_fields(statuses)) -end - -return M diff --git a/packages/site-board/departments/probe_scan/probe_scan_test.lua b/packages/site-board/departments/probe_scan/probe_scan_test.lua deleted file mode 100644 index fdc8d03..0000000 --- a/packages/site-board/departments/probe_scan/probe_scan_test.lua +++ /dev/null @@ -1,107 +0,0 @@ -local t = fkst.test -local core = require("core") - -local function nonce() - return tostring({}):gsub("[^%w._-]", "_") -end - -local function runtime_root(name) - return "/tmp/fkst-website-test/site-board-probe/" .. tostring(now()) .. "/" .. nonce() .. "/" .. name -end - -local function opts(name) - return { - env = { - FKST_RUNTIME_ROOT = runtime_root(name), - }, - } -end - -local function run_probe(run_opts) - return t.run_department("departments/probe_scan/main.lua", { - queue = "board_poll_tick", - payload = {}, - }, run_opts) -end - -local function mock_probe(path, code) - t.mock_command(core.curl_probe_cmd(path), { stdout = code, exit_code = 0 }) -end - -local function mock_probe_error(path) - t.mock_command(core.curl_probe_cmd(path), { stdout = "", stderr = "network down", exit_code = 7 }) -end - -local function github_issue_calls() - local calls = {} - for _, call in ipairs(t.command_calls()) do - if call.rendered:find("gh issue", 1, true) ~= nil or call.rendered:find("github_issue_create_request", 1, true) ~= nil then - table.insert(calls, call) - end - end - return calls -end - -return { - test_manifest_is_canonical_page_list = function() - local paths = core.read_probe_manifest("site/probe-manifest") - t.eq(#paths, 6) - t.eq(paths[1], "/") - t.eq(paths[2], "/zh/") - t.eq(paths[3], "/architecture.html") - t.eq(paths[4], "/doctrine.html") - t.eq(paths[5], "/zh/architecture.html") - t.eq(paths[6], "/zh/doctrine.html") - end, - - test_curl_probe_cmd_targets_published_site = function() - t.eq( - core.curl_probe_cmd("/architecture.html"), - "curl -sL -o /dev/null -w '%{http_code}' -- 'https://chronoaiproject.github.io/fkst-website/architecture.html'" - ) - local ok = pcall(core.curl_probe_cmd, "architecture.html") - t.eq(ok, false) - end, - - test_probe_all_200_logs_ok_without_issue_request = function() - mock_probe("/", "200") - mock_probe("/zh/", "200") - mock_probe("/architecture.html", "200") - mock_probe("/doctrine.html", "200") - mock_probe("/zh/architecture.html", "200") - mock_probe("/zh/doctrine.html", "200") - - local result = run_probe(opts("all-ok")) - t.eq(result.exit_code, 0) - t.eq(#result.raises, 0) - t.eq(#github_issue_calls(), 0) - end, - - test_probe_one_404_logs_failure_without_issue_request = function() - mock_probe("/", "200") - mock_probe("/zh/", "200") - mock_probe("/architecture.html", "404") - mock_probe("/doctrine.html", "200") - mock_probe("/zh/architecture.html", "200") - mock_probe("/zh/doctrine.html", "200") - - local result = run_probe(opts("one-404")) - t.eq(result.exit_code, 0) - t.eq(#result.raises, 0) - t.eq(#github_issue_calls(), 0) - end, - - test_probe_all_network_error_logs_skip_without_issue_request = function() - mock_probe_error("/") - mock_probe_error("/zh/") - mock_probe_error("/architecture.html") - mock_probe_error("/doctrine.html") - mock_probe_error("/zh/architecture.html") - mock_probe_error("/zh/doctrine.html") - - local result = run_probe(opts("all-network-error")) - t.eq(result.exit_code, 0) - t.eq(#result.raises, 0) - t.eq(#github_issue_calls(), 0) - end, -} From 74b27510f2f5c2a8df3035e3caad930a39d027fe Mon Sep 17 00:00:00 2001 From: auric Date: Wed, 10 Jun 2026 17:07:53 +0800 Subject: [PATCH 3/4] Fix github-devloop review feedback --- .github/workflows/pages.yml | 36 +------------ scripts/probe_site.sh | 47 +++++++++++++++++ scripts/probe_site_test.py | 101 ++++++++++++++++++++++++++++++++++++ scripts/run.sh | 1 + 4 files changed, 150 insertions(+), 35 deletions(-) create mode 100755 scripts/probe_site.sh create mode 100755 scripts/probe_site_test.py diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 6f654df..1a9d80e 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -36,38 +36,4 @@ jobs: uses: actions/deploy-pages@v4 - name: Live probe published site - run: | - set -euo pipefail - checked=0 - failures=0 - network_errors=0 - results=() - while IFS= read -r path || [ -n "$path" ]; do - path="${path%%#*}" - path="${path#"${path%%[![:space:]]*}"}" - path="${path%"${path##*[![:space:]]}"}" - [ -n "$path" ] || continue - checked=$((checked + 1)) - url="https://chronoaiproject.github.io/fkst-website${path}" - code="$(curl -sL -o /dev/null -w '%{http_code}' -- "$url")" || code="error" - [ -n "$code" ] || code="error" - results+=("${path}:${code}") - if [ "$code" = "200" ]; then - continue - elif [ "$code" = "error" ]; then - network_errors=$((network_errors + 1)) - else - failures=$((failures + 1)) - fi - done < site/probe-manifest - joined="$(IFS=,; echo "${results[*]}")" - if [ "$checked" -eq 0 ]; then - echo "fkst-website dept=pages tag=skip PROBE paths=0 reason=empty-manifest results=" - elif [ "$network_errors" -eq "$checked" ]; then - echo "fkst-website dept=pages tag=skip PROBE paths=$checked reason=all-paths-network-error results=$joined" - elif [ "$failures" -gt 0 ] || [ "$network_errors" -gt 0 ]; then - echo "fkst-website dept=pages tag=fail PROBE paths=$checked failures=$failures network_errors=$network_errors results=$joined" - exit 1 - else - echo "fkst-website dept=pages tag=ok PROBE paths=$checked results=$joined" - fi + run: scripts/probe_site.sh diff --git a/scripts/probe_site.sh b/scripts/probe_site.sh new file mode 100755 index 0000000..d9df2ff --- /dev/null +++ b/scripts/probe_site.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash +# Probe the deployed GitHub Pages site from the tracked manifest. +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +MANIFEST="${FKST_SITE_PROBE_MANIFEST:-$ROOT/site/probe-manifest}" +BASE_URL="${FKST_SITE_PROBE_BASE_URL:-https://chronoaiproject.github.io/fkst-website}" +TIMEOUT_SECONDS="${FKST_SITE_PROBE_TIMEOUT_SECONDS:-15}" +CONNECT_TIMEOUT_SECONDS="${FKST_SITE_PROBE_CONNECT_TIMEOUT_SECONDS:-5}" + +checked=0 +failures=0 +network_errors=0 +results=() + +while IFS= read -r path || [ -n "$path" ]; do + path="${path%%#*}" + path="${path#"${path%%[![:space:]]*}"}" + path="${path%"${path##*[![:space:]]}"}" + [ -n "$path" ] || continue + + checked=$((checked + 1)) + url="${BASE_URL}${path}" + code="$(curl -sL -o /dev/null -w '%{http_code}' --connect-timeout "$CONNECT_TIMEOUT_SECONDS" --max-time "$TIMEOUT_SECONDS" -- "$url")" || code="error" + [ -n "$code" ] || code="error" + results+=("${path}:${code}") + + if [ "$code" = "200" ]; then + continue + elif [ "$code" = "error" ]; then + network_errors=$((network_errors + 1)) + else + failures=$((failures + 1)) + fi +done < "$MANIFEST" + +joined="$(IFS=,; echo "${results[*]}")" +if [ "$checked" -eq 0 ]; then + echo "fkst-website dept=pages tag=skip PROBE paths=0 reason=empty-manifest results=" +elif [ "$network_errors" -eq "$checked" ]; then + echo "fkst-website dept=pages tag=skip PROBE paths=$checked reason=all-paths-network-error results=$joined" +elif [ "$failures" -gt 0 ] || [ "$network_errors" -gt 0 ]; then + echo "fkst-website dept=pages tag=fail PROBE paths=$checked failures=$failures network_errors=$network_errors results=$joined" + exit 1 +else + echo "fkst-website dept=pages tag=ok PROBE paths=$checked results=$joined" +fi diff --git a/scripts/probe_site_test.py b/scripts/probe_site_test.py new file mode 100755 index 0000000..0a9834c --- /dev/null +++ b/scripts/probe_site_test.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python3 +"""Unit tests for the deployed-site probe script.""" + +from __future__ import annotations + +import os +import stat +import subprocess +import tempfile +import textwrap +import unittest +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +PROBE = ROOT / "scripts" / "probe_site.sh" + + +class ProbeSiteTest(unittest.TestCase): + def run_probe(self, manifest: str, fake_curl: str) -> subprocess.CompletedProcess[str]: + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + manifest_path = tmp_path / "probe-manifest" + manifest_path.write_text(manifest, encoding="utf-8") + + bin_dir = tmp_path / "bin" + bin_dir.mkdir() + curl_path = bin_dir / "curl" + curl_path.write_text(fake_curl, encoding="utf-8") + curl_path.chmod(curl_path.stat().st_mode | stat.S_IXUSR) + + env = os.environ.copy() + env.update( + { + "PATH": f"{bin_dir}{os.pathsep}{env['PATH']}", + "FKST_SITE_PROBE_MANIFEST": str(manifest_path), + "FKST_SITE_PROBE_BASE_URL": "https://example.test", + } + ) + return subprocess.run( + [str(PROBE)], + cwd=ROOT, + env=env, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + check=False, + ) + + def test_all_200_logs_ok(self) -> None: + curl = textwrap.dedent( + """\ + #!/usr/bin/env bash + printf '200' + """ + ) + result = self.run_probe("/\n/zh/\n", curl) + + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual( + result.stdout.strip(), + "fkst-website dept=pages tag=ok PROBE paths=2 results=/:200,/zh/:200", + ) + + def test_partial_failure_logs_fail_and_exits_1(self) -> None: + curl = textwrap.dedent( + """\ + #!/usr/bin/env bash + url="${@: -1}" + case "$url" in + */missing.html) printf '404' ;; + *) printf '200' ;; + esac + """ + ) + result = self.run_probe("/\n/missing.html\n", curl) + + self.assertEqual(result.returncode, 1) + self.assertEqual( + result.stdout.strip(), + "fkst-website dept=pages tag=fail PROBE paths=2 failures=1 network_errors=0 results=/:200,/missing.html:404", + ) + + def test_all_network_error_logs_skip(self) -> None: + curl = textwrap.dedent( + """\ + #!/usr/bin/env bash + exit 7 + """ + ) + result = self.run_probe("/\n/zh/\n", curl) + + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual( + result.stdout.strip(), + "fkst-website dept=pages tag=skip PROBE paths=2 reason=all-paths-network-error results=/:error,/zh/:error", + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/scripts/run.sh b/scripts/run.sh index cc27be2..1e3ca7c 100755 --- a/scripts/run.sh +++ b/scripts/run.sh @@ -124,6 +124,7 @@ usage() { cmd_check() { python3 "$ROOT/scripts/check_repo.py" python3 "$ROOT/scripts/check_repo_test.py" + python3 "$ROOT/scripts/probe_site_test.py" } check_test_file_coverage() { From 67084f59938c33a02fced06c6b9088e9570df5f0 Mon Sep 17 00:00:00 2001 From: auric Date: Wed, 10 Jun 2026 20:16:52 +0800 Subject: [PATCH 4/4] nudge: re-trigger review after stranded pr-open (see fkst-packages#175)