deploy, devtools: centralize and parameterize Go version management

Parameterize all Cloud Build configurations (deploy.yaml,
deploy-env.yaml, migrate.yaml, sitemap.yaml) by adding a single
_GO_VERSION substitution and referencing it in build steps.

Pass GO_VERSION=$_GO_VERSION in Cloud Build steps that execute docker
compose. In devtools/docker.sh, define GO_VERSION with a fallback
default to 1.26.4. This allows docker-compose services to use the
correct Go version without hardcoding the version string multiple times
or tying it to the go.mod version.

Source devtools/docker.sh in tests/screentest/run.sh so that commands
invoking docker compose directly will have GO_VERSION set.

Document the centralized version management in deploy/README.md, and
update the outdated Go version reference in CONTRIBUTING.md.

For golang/go#72774

Change-Id: I8d7067a63a20be1c30818a4c6098e4578a0b30e2
Reviewed-on: https://go-review.googlesource.com/c/pkgsite/+/791760
kokoro-CI: kokoro <noreply+kokoro@google.com>
Reviewed-by: Nicholas Husin <nsh@golang.org>
Reviewed-by: Nicholas Husin <husin@google.com>
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Dmitri Shuralyov <dmitshur@golang.org>
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 73e494b..b53f567 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -37,8 +37,9 @@
 
 2. Download the source code for x/pkgsite:
    `git clone https://go.googlesource.com/pkgsite`
-   NOTE: You will need Go 1.19 to
-   develop pkgsite.
+   NOTE: While recent Go versions (1.21+) will automatically upgrade to the
+   version specified in go.mod, it is recommended to use the latest stable Go
+   version for development.
 
 3. Review the [design document](doc/design.md).
 
diff --git a/deploy/README.md b/deploy/README.md
index 88fc91c..e0324f1 100644
--- a/deploy/README.md
+++ b/deploy/README.md
@@ -11,3 +11,35 @@
 
 `deploy.yaml` deploys to staging, runs e2e tests, and if the tests pass,
 deploys to production.
+
+## Go Version Management
+
+The Go runtime version used for deployments and docker environments is managed independently from the `go.mod` language version constraint.
+
+### Cloud Build Configuration
+
+The Go image version is parameterized in all Cloud Build files (`deploy/*.yaml`) using a `_GO_VERSION` substitution:
+```yaml
+substitutions:
+  _GO_VERSION: 1.26.4
+```
+This version is used by all Go build steps via `golang:$_GO_VERSION`. To update the version for GCP Cloud Build deployments, update `_GO_VERSION` in the `substitutions:` block of:
+- `deploy/deploy.yaml`
+- `deploy/deploy-env.yaml`
+- `deploy/migrate.yaml`
+- `deploy/sitemap.yaml`
+
+### Docker Compose
+
+For local development and testing under docker, the Go version is defined in `devtools/docker.sh`:
+```bash
+export GO_VERSION=${GO_VERSION:-1.26.4}
+```
+Docker Compose configuration (`devtools/docker/compose.yaml`) references the `${GO_VERSION}` environment variable.
+
+During Cloud Build execution, the `all.bash` step explicitly passes `GO_VERSION=$_GO_VERSION` to the Docker Compose environment to ensure the local container testing matches the Cloud Build deployment environment:
+```yaml
+    env:
+      - GO_VERSION=$_GO_VERSION
+```
+
diff --git a/deploy/deploy-env.yaml b/deploy/deploy-env.yaml
index 2a1f2ba..92a31ca 100644
--- a/deploy/deploy-env.yaml
+++ b/deploy/deploy-env.yaml
@@ -16,19 +16,22 @@
       - run
       - allbash
       - ci
+    env:
+      # Pass the Cloud Build Go version parameter down to Docker Compose to ensure consistency.
+      - GO_VERSION=$_GO_VERSION
   - id: Prepare
     name: gcr.io/cloud-builders/gcloud
     entrypoint: deploy/prepare.sh
     args:
       - $PROJECT_ID
   - id: Tidy private
-    name: golang:1.26.4
+    name: golang:$_GO_VERSION
     entrypoint: sh
     args:
       - -c
       - cd private && go mod tidy
   - id: Check redistributable
-    name: golang:1.26.4
+    name: golang:$_GO_VERSION
     entrypoint: private/devtools/check_redist.sh
   - id: Build
     name: gcr.io/cloud-builders/docker
@@ -56,7 +59,11 @@
       - -c
       - deploy/frontend.sh $_ENV gcr.io/$PROJECT_ID/frontend:$(cat _BUILD_TAG)
   - id: Pagecheck
-    name: golang:1.26.4
+    name: golang:$_GO_VERSION
     entrypoint: deploy/pagecheck.sh
     args:
       - $_ENV
+
+substitutions:
+  _GO_VERSION: 1.26.4
+
diff --git a/deploy/deploy.yaml b/deploy/deploy.yaml
index 900652b..9eeea2c 100644
--- a/deploy/deploy.yaml
+++ b/deploy/deploy.yaml
@@ -16,19 +16,22 @@
       - run
       - allbash
       - ci
+    env:
+      # Pass the Cloud Build Go version parameter down to Docker Compose to ensure consistency.
+      - GO_VERSION=$_GO_VERSION
   - id: Prepare
     name: gcr.io/cloud-builders/gcloud
     entrypoint: deploy/prepare.sh
     args:
       - $PROJECT_ID
   - id: Tidy private
-    name: golang:1.26.4
+    name: golang:$_GO_VERSION
     entrypoint: sh
     args:
       - -c
       - cd private && go mod tidy
   - id: Check redistributable
-    name: golang:1.26.4
+    name: golang:$_GO_VERSION
     entrypoint: private/devtools/check_redist.sh
   - id: Build
     name: gcr.io/cloud-builders/docker
@@ -56,7 +59,7 @@
       - -c
       - deploy/frontend.sh staging gcr.io/$PROJECT_ID/frontend:$(cat _BUILD_TAG)
   - id: Pagecheck - staging
-    name: golang:1.26.4
+    name: golang:$_GO_VERSION
     entrypoint:  deploy/pagecheck.sh
     args:
       - staging
@@ -87,7 +90,7 @@
       - -c
       - deploy/frontend.sh prod gcr.io/$PROJECT_ID/frontend:$(cat _BUILD_TAG)
   - id: Pagecheck - prod
-    name: golang:1.26.4
+    name: golang:$_GO_VERSION
     entrypoint:  deploy/pagecheck.sh
     args:
       - prod
@@ -97,3 +100,7 @@
     paths:
       - 'tests/screentest/output/*'
       - 'tests/screentest/output/testcases/*'
+
+substitutions:
+  _GO_VERSION: 1.26.4
+
diff --git a/deploy/migrate.yaml b/deploy/migrate.yaml
index 9a0b0da..24640a5 100644
--- a/deploy/migrate.yaml
+++ b/deploy/migrate.yaml
@@ -18,7 +18,7 @@
     args:
       - $PROJECT_ID
   - id: Migrate
-    name: golang:1.26.4
+    name: golang:$_GO_VERSION
     entrypoint: bash
     dir: private
     args:
@@ -39,3 +39,7 @@
   secretManager:
   - versionName: projects/$PROJECT_ID/secrets/cloudbuild-database-password/versions/latest
     env: 'PASSWORD'
+
+substitutions:
+  _GO_VERSION: 1.26.4
+
diff --git a/deploy/sitemap.yaml b/deploy/sitemap.yaml
index e12c5ce..7bb562e 100644
--- a/deploy/sitemap.yaml
+++ b/deploy/sitemap.yaml
@@ -9,7 +9,7 @@
     args:
       - $PROJECT_ID
   - id: Build
-    name: golang:1.26.4
+    name: golang:$_GO_VERSION
     entrypoint: bash
     dir: private
     args:
@@ -39,3 +39,7 @@
   secretManager:
   - versionName: projects/$PROJECT_ID/secrets/cloudbuild-database-password/versions/latest
     env: 'PASSWORD'
+
+substitutions:
+  _GO_VERSION: 1.26.4
+
diff --git a/devtools/docker.sh b/devtools/docker.sh
index a71d9e0..d373d70 100644
--- a/devtools/docker.sh
+++ b/devtools/docker.sh
@@ -4,6 +4,9 @@
 
 # Library of useful docker functions and variables.
 
+export GO_VERSION=${GO_VERSION:-1.26.4}
+
+
 docker_cleanup() {
   if [ "$GO_DISCOVERY_DOCKER_SKIP_CLEANUP" = "true" ]; then
     echo "Skipping docker cleanup because GO_DISCOVERY_DOCKER_SKIP_CLEANUP=true."
diff --git a/devtools/docker/compose.yaml b/devtools/docker/compose.yaml
index a14fce4..26e612c 100644
--- a/devtools/docker/compose.yaml
+++ b/devtools/docker/compose.yaml
@@ -30,7 +30,7 @@
     depends_on:
       - db
      # This should match the version we are using on Cloud Run.
-    image: golang:1.26.4
+    image: golang:${GO_VERSION}
     environment:
       <<: [*database-variables, *go-variables]
       GO_DISCOVERY_TESTDB: ${GO_DISCOVERY_TESTDB:-"true"}
@@ -41,7 +41,7 @@
       - ../../:/pkgsite
     working_dir: /pkgsite
   searchtest:
-    image: golang:1.26.4
+    image: golang:${GO_VERSION}
     depends_on:
       - frontend
     environment:
@@ -52,7 +52,7 @@
       - ../../:/pkgsite
     working_dir: /pkgsite
   api:
-    image: golang:1.26.4
+    image: golang:${GO_VERSION}
     depends_on:
       - frontend
     environment:
@@ -64,7 +64,7 @@
     working_dir: /pkgsite
   frontend:
      # This should match the version we are using on AppEngine.
-    image: golang:1.26.4
+    image: golang:${GO_VERSION}
     depends_on:
       - db
     command: bash -c "
@@ -81,7 +81,7 @@
     working_dir: /pkgsite
   seeddb:
      # This should match the version we are using on Cloud Run.
-    image: golang:1.26.4
+    image: golang:${GO_VERSION}
     depends_on:
       - db
     # Note: technically we should check that migrations have completed before
@@ -134,7 +134,7 @@
     shm_size: 8G
   go:
      # This should match the version we are using on Cloud Run.
-    image: golang:1.26.4
+    image: golang:${GO_VERSION}
     entrypoint: go
     environment:
       <<: [*database-variables, *go-variables]
diff --git a/tests/screentest/run.sh b/tests/screentest/run.sh
index 3a961b7..397ab7a 100755
--- a/tests/screentest/run.sh
+++ b/tests/screentest/run.sh
@@ -7,12 +7,14 @@
 set -e
 
 source devtools/lib.sh || { echo "Are you at repo root?"; exit 1; }
+source devtools/docker.sh || { echo "Are you at repo root?"; exit 1; }
+
 
 # Do not truncate commands when displaying them (see runcmd in devtools/lib.sh).
 MAXWIDTH=0
 
 screentest_version=latest
- 
+
 # This should match the version we are using in devtools/docker/compose.yaml.
 chromedp_version=131.0.6778.33