Fix restricted map controls and release verification
All checks were successful
Deploy MineGeo to Dokploy / deploy (push) Successful in 5m17s
All checks were successful
Deploy MineGeo to Dokploy / deploy (push) Successful in 5m17s
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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({
|
||||
<div className="map-surface">
|
||||
<div
|
||||
ref={root}
|
||||
tabIndex={0}
|
||||
style={{ height: "100%", width: "100%" }}
|
||||
role="application"
|
||||
aria-label={
|
||||
|
||||
@@ -21,13 +21,31 @@ function inline(text: string) {
|
||||
}
|
||||
|
||||
export function ReleaseNotes({ body }: { body: string }) {
|
||||
return body.split(/\r?\n\s*\r?\n/).map((block, i) => {
|
||||
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 (
|
||||
<ul key={i}>
|
||||
{lines.map((line, j) => (
|
||||
<li key={j}>{inline(line.replace(/^\s*[-*]\s+/, ""))}</li>
|
||||
<li key={j}>{inline(line)}</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
|
||||
@@ -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
|
||||
|
||||
12
docs/releases/1.0.6-beta.md
Normal file
12
docs/releases/1.0.6-beta.md
Normal file
@@ -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: <https://minegeo.site/>
|
||||
90
scripts/dokploy-deployment-state.mjs
Normal file
90
scripts/dokploy-deployment-state.mjs
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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, /<p>This release improves:<\/p><ul>/);
|
||||
assert.match(
|
||||
html,
|
||||
/<li><strong>Controls:<\/strong> Keyboard access and fixed map gestures\.<\/li>/,
|
||||
);
|
||||
assert.match(
|
||||
html,
|
||||
/<li>Changelog lists\.<\/li><\/ul><p>More updates soon\.<\/p>/,
|
||||
);
|
||||
});
|
||||
|
||||
54
tests/dokploy-deployment-state.test.ts
Normal file
54
tests/dokploy-deployment-state.test.ts
Normal file
@@ -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);
|
||||
});
|
||||
Reference in New Issue
Block a user