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({
This release improves:<\/p>
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); +});