From 690fe12c2c468c743a3e58421247ec347f9bee53 Mon Sep 17 00:00:00 2001 From: pavel444-byte Date: Tue, 20 Oct 2026 16:46:36 +0500 Subject: [PATCH] Fix restricted map controls and release verification --- .gitea/workflows/deploy-dokploy.yml | 54 +++++----------- components/GuessMap.tsx | 9 +-- components/ReleaseNotes.tsx | 26 ++++++-- docs/dokploy-deployment.md | 7 +- docs/releases/1.0.6-beta.md | 12 ++++ scripts/dokploy-deployment-state.mjs | 90 ++++++++++++++++++++++++++ tests/changelog.test.ts | 17 +++++ tests/dokploy-deployment-state.test.ts | 54 ++++++++++++++++ 8 files changed, 222 insertions(+), 47 deletions(-) create mode 100644 docs/releases/1.0.6-beta.md create mode 100644 scripts/dokploy-deployment-state.mjs create mode 100644 tests/dokploy-deployment-state.test.ts diff --git a/.gitea/workflows/deploy-dokploy.yml b/.gitea/workflows/deploy-dokploy.yml index 19774c9..b3beb1a 100644 --- a/.gitea/workflows/deploy-dokploy.yml +++ b/.gitea/workflows/deploy-dokploy.yml @@ -91,19 +91,7 @@ jobs: --connect-timeout 10 --max-time 30 \ "$DOKPLOY_URL/api/deployment.all" - previous_id="$(DOKPLOY_STATUS_FILE="$deployment_file" node <<'NODE' - const fs = require("node:fs"); - let payload; - try { payload = JSON.parse(fs.readFileSync(process.env.DOKPLOY_STATUS_FILE, "utf8")); } - catch { console.error("Dokploy deployment list was not valid JSON."); process.exit(2); } - const rows = Array.isArray(payload) - ? payload - : [payload?.data, payload?.deployments, payload?.items].find(Array.isArray); - if (!rows) process.exit(2); - const latest = rows[0]; - process.stdout.write(String(latest?.id ?? latest?.deploymentId ?? "")); - NODE - )" || { + previous_ids="$(node scripts/dokploy-deployment-state.mjs snapshot "$deployment_file")" || { node scripts/dokploy-diagnostics.mjs request "API preflight response parsing" parse-error \ "$metrics_file" "$error_file" "$headers_file" "$deployment_file" "$auth_file" >&2 echo "Dokploy returned an unexpected deployment list; source was not uploaded." >&2 @@ -118,34 +106,25 @@ jobs: echo "Dokploy accepted the source archive; waiting for the VPS build and deployment." attempt=0 + poll_failures=0 while [ "$attempt" -lt 360 ]; do - dokploy_request "Deployment status GET /api/deployment.all (attempt $((attempt + 1))/360)" "$deployment_file" --get \ + attempt=$((attempt + 1)) + if dokploy_request "Deployment status GET /api/deployment.all (attempt $attempt/360)" "$deployment_file" --get \ --data-urlencode "applicationId=$DOKPLOY_APPLICATION_ID" \ --connect-timeout 10 --max-time 30 \ - "$DOKPLOY_URL/api/deployment.all" + "$DOKPLOY_URL/api/deployment.all"; then + poll_failures=0 + else + poll_failures=$((poll_failures + 1)) + if node scripts/dokploy-deployment-state.mjs retry "$curl_exit" "$http_code" "$poll_failures"; then + echo "Transient status lookup failure ($poll_failures/6); the accepted build may still be running. Retrying only the GET." + sleep 10 + continue + fi + exit 1 + fi - status="$(DOKPLOY_STATUS_FILE="$deployment_file" DOKPLOY_PREVIOUS_DEPLOYMENT_ID="$previous_id" node <<'NODE' - const fs = require("node:fs"); - let payload; - try { payload = JSON.parse(fs.readFileSync(process.env.DOKPLOY_STATUS_FILE, "utf8")); } - catch { console.error("Dokploy deployment status was not valid JSON."); process.exit(2); } - const rows = Array.isArray(payload) - ? payload - : [payload?.data, payload?.deployments, payload?.items].find(Array.isArray); - if (!rows) process.exit(2); - const previousId = process.env.DOKPLOY_PREVIOUS_DEPLOYMENT_ID; - const current = rows.find((row) => { - const id = String(row?.id ?? row?.deploymentId ?? ""); - return id && id !== previousId; - }); - if (!current) { - process.stdout.write("waiting"); - } else { - const value = String(current.status ?? "unknown").toLowerCase(); - process.stdout.write(["done", "error", "running", "queued", "building", "deploying", "pending", "cancelled", "canceled"].includes(value) ? value : "unknown"); - } - NODE - )" || { + status="$(DOKPLOY_PREVIOUS_DEPLOYMENT_IDS="$previous_ids" node scripts/dokploy-deployment-state.mjs status "$deployment_file")" || { node scripts/dokploy-diagnostics.mjs request "Deployment status response parsing" parse-error \ "$metrics_file" "$error_file" "$headers_file" "$deployment_file" "$auth_file" >&2 echo "Dokploy returned an unexpected deployment status; inspect the response diagnostics above." >&2 @@ -169,7 +148,6 @@ jobs: esac sleep 10 - attempt=$((attempt + 1)) done echo "Timed out waiting for Dokploy to finish; inspect the deployment in Dokploy." >&2 diff --git a/components/GuessMap.tsx b/components/GuessMap.tsx index b658f38..6710387 100644 --- a/components/GuessMap.tsx +++ b/components/GuessMap.tsx @@ -66,10 +66,10 @@ export function GuessMap({ maxBounds: b.pad(0.15), attributionControl: false, zoomControl: false, - scrollWheelZoom: zoom, - touchZoom: zoom, - doubleClickZoom: zoom, - boxZoom: zoom, + scrollWheelZoom: zoom && (pan ? true : "center"), + touchZoom: zoom && (pan ? true : "center"), + doubleClickZoom: zoom && (pan ? true : "center"), + boxZoom: zoom && pan, dragging: pan, keyboard: false, }); @@ -232,6 +232,7 @@ export function GuessMap({
{ - const lines = block.split(/\r?\n/); - if (lines.every((line) => /^\s*[-*]\s+/.test(line))) + const blocks: { list: boolean; lines: string[] }[] = []; + let current: (typeof blocks)[number] | undefined; + for (const line of body.split(/\r?\n/)) { + if (!line.trim()) { + current = undefined; + continue; + } + const bullet = /^\s*[-*]\s+(.+)$/.exec(line); + if (!bullet && current?.list && /^\s{2,}\S/.test(line)) { + current.lines[current.lines.length - 1] += ` ${line.trim()}`; + continue; + } + const list = !!bullet; + if (!current || current.list !== list) { + current = { list, lines: [] }; + blocks.push(current); + } + current.lines.push(bullet ? bullet[1] : line); + } + return blocks.map(({ list, lines }, i) => { + if (list) return (
    {lines.map((line, j) => ( -
  • {inline(line.replace(/^\s*[-*]\s+/, ""))}
  • +
  • {inline(line)}
  • ))}
); diff --git a/docs/dokploy-deployment.md b/docs/dokploy-deployment.md index a39faad..8ab063f 100644 --- a/docs/dokploy-deployment.md +++ b/docs/dokploy-deployment.md @@ -130,7 +130,12 @@ does not enable shell tracing, disable TLS verification, follow authenticated redirects, or retry source uploads automatically. HTTP `000` means no HTTP response was received; inspect the curl exit code and error immediately above it. The job waits for Dokploy's deployment status and fails if the upload or deployment -fails. Build logs remain available in the Dokploy dashboard. +fails. Status polling tolerates up to six consecutive transient network/server +failures after an upload has been accepted, and retries only the read-only status +request. Authentication and certificate verification errors still stop the job. +All deployment IDs present before the upload are excluded from completion checks, +so an older successful build cannot be mistaken for the new release. Build logs +remain available in the Dokploy dashboard. `git archive` sends tracked files from that commit only. It does not include `.git` metadata, local environment files, untracked files, or local caches. The diff --git a/docs/releases/1.0.6-beta.md b/docs/releases/1.0.6-beta.md new file mode 100644 index 0000000..26ec135 --- /dev/null +++ b/docs/releases/1.0.6-beta.md @@ -0,0 +1,12 @@ +# MineGeo 1.0.6 Beta — Void Update + +The hardest expedition yet, more control over custom challenges, and release notes right on the site. 😄 + +- **Void difficulty:** Take on 10-second rounds with an End-purple glow, a fixed panorama and map, no zoom, compass, or coordinates, and strict scoring. Available in solo, multiplayer, and tournaments. +- **More custom settings:** Control guess-map panning and countdown visibility, and choose timers from 5 to 600 seconds in 5-second steps. +- **Changelogs:** Read release history on the new Changelogs page. It automatically reads release webhook posts from Discord news and removes role mentions and the play-site footer. +- **Control fixes:** Fixed keyboard access and Enter-to-pin on restricted maps. Disabling map panning now also blocks movement through zoom gestures. +- **Display fixes:** Improved release-note bullet formatting and kept navigation readable in English and Russian. +- **Deployment fixes:** Status checks tolerate temporary network failures and cannot mistake an older successful build for the new release. + +PLAY THE MINEGEO AT: diff --git a/scripts/dokploy-deployment-state.mjs b/scripts/dokploy-deployment-state.mjs new file mode 100644 index 0000000..f9ec35d --- /dev/null +++ b/scripts/dokploy-deployment-state.mjs @@ -0,0 +1,90 @@ +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +export function deploymentRows(payload) { + const rows = Array.isArray(payload) + ? payload + : [payload?.data, payload?.deployments, payload?.items].find(Array.isArray); + if (!rows || rows.some((row) => !row || typeof row !== "object")) + throw new Error("Unexpected Dokploy deployment response"); + return rows; +} + +const identity = (row) => String(row.id ?? row.deploymentId ?? ""); +export function knownDeploymentIds(payload) { + return deploymentRows(payload).map(identity).filter(Boolean); +} + +export function deploymentState(payload, previousIds) { + if ( + !Array.isArray(previousIds) || + previousIds.some((id) => typeof id !== "string") + ) + throw new Error("Invalid deployment baseline"); + const known = new Set(previousIds); + const current = deploymentRows(payload).find( + (row) => identity(row) && !known.has(identity(row)), + ); + if (!current) return "waiting"; + const status = String(current.status ?? "unknown").toLowerCase(); + return [ + "done", + "error", + "running", + "queued", + "building", + "deploying", + "pending", + "cancelled", + "canceled", + ].includes(status) + ? status + : "unknown"; +} + +export function retryableStatusPoll(curlExit, httpCode, consecutiveFailures) { + if ( + !Number.isInteger(consecutiveFailures) || + consecutiveFailures < 1 || + consecutiveFailures > 6 + ) + return false; + return ( + [7, 28, 52, 55, 56].includes(curlExit) || + (curlExit === 0 && [408, 429, 500, 502, 503, 504].includes(httpCode)) + ); +} + +if ( + process.argv[1] && + fileURLToPath(import.meta.url) === resolve(process.argv[1]) +) { + try { + const [mode, file, code, failures] = process.argv.slice(2); + if (mode === "retry") { + process.exitCode = retryableStatusPoll( + Number(file), + Number(code), + Number(failures), + ) + ? 0 + : 1; + } else { + const payload = JSON.parse(readFileSync(file, "utf8")); + if (mode === "snapshot") + process.stdout.write(JSON.stringify(knownDeploymentIds(payload))); + else if (mode === "status") + process.stdout.write( + deploymentState( + payload, + JSON.parse(process.env.DOKPLOY_PREVIOUS_DEPLOYMENT_IDS ?? "null"), + ), + ); + else throw new Error("Unknown mode"); + } + } catch { + console.error("Unexpected Dokploy deployment response or baseline."); + process.exitCode = 2; + } +} diff --git a/tests/changelog.test.ts b/tests/changelog.test.ts index c68285b..b18acb2 100644 --- a/tests/changelog.test.ts +++ b/tests/changelog.test.ts @@ -134,3 +134,20 @@ test("release Markdown renders emphasis and safe links, escaping executable HTML assert.match(html, /href="https:\/\/minegeo.site\/docs"/); assert.doesNotMatch(html, /href="javascript:/); }); + +test("release lists directly after a paragraph and wrapped bullet text remain lists", () => { + const html = renderToStaticMarkup( + createElement(ReleaseNotes, { + body: "This release improves:\n- **Controls:** Keyboard access\n and fixed map gestures.\n- Changelog lists.\nMore updates soon.", + }), + ); + assert.match(html, /

This release improves:<\/p>

    /); + assert.match( + html, + /
  • Controls:<\/strong> Keyboard access and fixed map gestures\.<\/li>/, + ); + assert.match( + html, + /
  • Changelog lists\.<\/li><\/ul>

    More updates soon\.<\/p>/, + ); +}); diff --git a/tests/dokploy-deployment-state.test.ts b/tests/dokploy-deployment-state.test.ts new file mode 100644 index 0000000..8153028 --- /dev/null +++ b/tests/dokploy-deployment-state.test.ts @@ -0,0 +1,54 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { + deploymentState, + knownDeploymentIds, + retryableStatusPoll, +} from "../scripts/dokploy-deployment-state.mjs"; + +test("deployment monitoring cannot mistake any older successful build for the new upload", () => { + const baseline = [ + { id: "previous", status: "done" }, + { deploymentId: "older", status: "done" }, + ]; + const ids = knownDeploymentIds({ deployments: baseline }); + assert.equal(deploymentState(baseline, ids), "waiting"); + assert.equal(deploymentState({ data: [baseline[1]] }, ids), "waiting"); + assert.equal( + deploymentState([{ id: "new", status: "running" }, ...baseline], ids), + "running", + ); + assert.equal( + deploymentState([{ id: "new", status: "done" }, ...baseline], ids), + "done", + ); + assert.equal( + deploymentState([{ id: "new", status: "error" }, ...baseline], ids), + "error", + ); + assert.equal( + deploymentState([{ id: "new", status: "cancelled" }], ids), + "cancelled", + ); +}); + +test("deployment monitoring rejects malformed responses and baselines", () => { + assert.throws(() => knownDeploymentIds({ error: "Unauthorized" })); + assert.throws(() => deploymentState([null], [])); + assert.throws(() => deploymentState([], "previous")); + assert.equal( + deploymentState([{ id: "first", status: "new-status" }], []), + "unknown", + ); +}); + +test("only bounded transient read-only status failures retry; auth and TLS verification errors stop", () => { + for (const exit of [7, 28, 52, 55, 56]) + assert.equal(retryableStatusPoll(exit, 0, 1), true); + for (const code of [408, 429, 500, 502, 503, 504]) + assert.equal(retryableStatusPoll(0, code, 6), true); + for (const code of [400, 401, 403, 404]) + assert.equal(retryableStatusPoll(0, code, 1), false); + assert.equal(retryableStatusPoll(60, 0, 1), false); + assert.equal(retryableStatusPoll(28, 0, 7), false); +});