From c3a706b4af3fce5ebe32543fd798f8c10ddcd4cb Mon Sep 17 00:00:00 2001 From: Markus Neusinger <2921697+MarkusNeusinger@users.noreply.github.com> Date: Tue, 18 Aug 2026 21:25:47 +0200 Subject: [PATCH 1/2] fix(seo): make the trailing-slash redirect relative MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #10473 removed a redirect that leaked http://api.anyplot.ai/seo-proxy/… and shipped one that leaks http://anyplot.ai:8080/… instead. Smaller, but the same defect: nginx builds a `permanent` rewrite's Location from $scheme://$host:$server_port, and behind Cloud Run that is plain http on port 8080. Verified live after the deploy — /box-basic/ answered 301 Location: http://anyplot.ai:8080/box-basic absolute_redirect off makes the Location relative. The client resolves it against the URL it actually requested, which is the only value in this setup guaranteed to be correct — hardcoding a scheme and host would work today and rot the next time either changes. The daily bot-serving-check would have caught it on its next run: the guard added in #10473 already rejects an http:// target. It now rejects an internal port explicitly as well, so the next variant of this does not need the scheme to be wrong before it fails. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/bot-serving-check.yml | 2 +- CHANGELOG.md | 9 +++++++++ app/nginx.conf | 9 +++++++++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/.github/workflows/bot-serving-check.yml b/.github/workflows/bot-serving-check.yml index e955dafbe6..6b3e3e23a1 100644 --- a/.github/workflows/bot-serving-check.yml +++ b/.github/workflows/bot-serving-check.yml @@ -108,7 +108,7 @@ jobs: slash_target=$(curl -sS --max-time 30 -o /dev/null -A "$GOOGLEBOT" \ -w '%{redirect_url}' "$ORIGIN/scatter-basic/") case "$slash_target" in - *"/seo-proxy"*|http://*) + *"/seo-proxy"*|http://*|*:8080/*) echo "::error::trailing-slash redirect leaks or downgrades: $slash_target" fail=1 ;; *) echo "OK: trailing slash -> $slash_target" ;; diff --git a/CHANGELOG.md b/CHANGELOG.md index b1d7c37945..2146a42f44 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -232,6 +232,15 @@ aggregate instead: an italic *Catalog* line at the end of the version section an ### Fixed +- **The trailing-slash redirect leaked the internal port** — #10473 removed a redirect to + `http://api.anyplot.ai/seo-proxy/…` and replaced it with `http://anyplot.ai:8080/…`: a smaller + version of the same defect, since nginx builds a `permanent` rewrite's Location from + `$scheme://$host:$server_port`, and behind Cloud Run that is plain http on port 8080. + `absolute_redirect off` makes the Location relative, which the client resolves against the URL it + actually requested — the only value here guaranteed to be right. The daily `bot-serving-check` + would have caught this on its next run, since it already rejects an `http://` target; it now + rejects an internal port explicitly too (#10476). + - **Preview images were forbidden to every crawler that follows the rules** — `api.anyplot.ai` served a blanket `Disallow: /`, while every prerendered page references its preview image at `api.anyplot.ai/og/…png`. The image answered HTTP 200 and the host forbade fetching it, so an diff --git a/app/nginx.conf b/app/nginx.conf index 8233ddb18c..d64e572806 100644 --- a/app/nginx.conf +++ b/app/nginx.conf @@ -175,6 +175,15 @@ server { # 48 "Redirect error" URLs in Search Console got there. # # `/` itself cannot match: after `^/` there is nothing left for `/$`. + # + # absolute_redirect off is load-bearing, not tidiness. nginx builds the + # Location of a `permanent` rewrite from $scheme://$host:$server_port, and + # behind Cloud Run that is http and port 8080 — so the first version of + # this fix emitted `http://anyplot.ai:8080/box-basic`, trading the + # api-host leak it removed for an internal-port one. A relative Location + # is resolved by the client against the URL it actually requested, which + # is the only value here that is guaranteed correct. + absolute_redirect off; rewrite ^/(.*)/$ /$1 permanent; # Disable caching for index.html From ceb87dec4896f8ec1a807ecaeec28c283ff79e55 Mon Sep 17 00:00:00 2001 From: Markus Neusinger <2921697+MarkusNeusinger@users.noreply.github.com> Date: Tue, 18 Aug 2026 21:29:56 +0200 Subject: [PATCH 2/2] docs(deploy): write down that the build triggers are regional MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `gcloud builds list --project=anyplot` returns three builds from early 2026 and nothing since, because the deploy triggers run in europe-west4 and regional builds are absent from the global list. Read at face value that says nothing has deployed for months, which is what happened in this session: I reported the SEO work as merged-but-not-live, and the owner had to point at a successful build to correct me. Every merge had in fact deployed within about five minutes. The guide now carries the `--region` form of both commands, notes that notify-deployment.yml only records a GitHub deployment rather than performing one, and says plainly that the trustworthy check is probing production — the only method that also catches a build succeeding while shipping the wrong thing, which is precisely what this PR fixes. Co-Authored-By: Claude Opus 5 (1M context) --- CHANGELOG.md | 6 +++++- agentic/docs/project-guide.md | 39 +++++++++++++++++++++++++++++++++-- 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2146a42f44..83c2fc8df5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -239,7 +239,11 @@ aggregate instead: an italic *Catalog* line at the end of the version section an `absolute_redirect off` makes the Location relative, which the client resolves against the URL it actually requested — the only value here guaranteed to be right. The daily `bot-serving-check` would have caught this on its next run, since it already rejects an `http://` target; it now - rejects an internal port explicitly too (#10476). + rejects an internal port explicitly too. `agentic/docs/project-guide.md` also gains the step that + makes this checkable at all: the deploy triggers are **regional** (`europe-west4`), so a + `gcloud builds list` without `--region` returns builds from early 2026 and reads as "nothing has + deployed for months" — a wrong conclusion drawn in this session and corrected by the repo owner + (#10476). - **Preview images were forbidden to every crawler that follows the rules** — `api.anyplot.ai` served a blanket `Disallow: /`, while every prerendered page references its preview image at diff --git a/agentic/docs/project-guide.md b/agentic/docs/project-guide.md index dcaa0b3760..114a0cb699 100644 --- a/agentic/docs/project-guide.md +++ b/agentic/docs/project-guide.md @@ -890,8 +890,43 @@ The project runs on **Google Cloud Platform**: ### Automatic Deployment Push to `main` branch triggers Cloud Build: -- Changes in `api/`, `core/`, `pyproject.toml` -> Backend redeploy -- Changes in `app/` -> Frontend redeploy +- Changes in `api/`, `core/`, `pyproject.toml` -> Backend redeploy (`deploy-api`) +- Changes in `app/` -> Frontend redeploy (`deploy-app`) + +Roughly five minutes from merge to live, per build. + +#### Checking whether something actually deployed + +The triggers are **regional, in `europe-west4`**, and regional builds do not +appear in the global build list. Always pass `--region`: + +```bash +gcloud builds list --region=europe-west4 --project=anyplot --limit=10 \ + --format="table(id.slice(0,8),status,createTime.date('%H:%M'),substitutions.TRIGGER_NAME,substitutions.SHORT_SHA)" +gcloud builds describe --region=europe-west4 --project=anyplot +``` + +Omitting `--region` returns a handful of builds from early 2026 and nothing +since — which reads as "nothing has deployed for months" and is simply the +wrong list. That mistake has been made and had to be corrected by the repo +owner. + +`.github/workflows/notify-deployment.yml` only **records** a GitHub deployment; +it does not deploy anything. A green run there says nothing about whether the +code is live. + +The most trustworthy check is neither of the above: probe production and +compare against the expected behaviour, which is the only method that survives +a build succeeding while shipping the wrong thing. + +```bash +curl -A "Mozilla/5.0 (compatible; Googlebot/2.1)" -sSI https://anyplot.ai/box-basic/ | head -3 +curl -s https://anyplot.ai/robots.txt | head -5 +``` + +This matters most for `app/nginx.conf`: nginx behaviour has no local +verification loop in this repo, so production is the first place a change is +observable at all. ### Manual Deployment