cmd/golangorg: simplify, run on App Engine Standard, not Flex

This app has been through a lot of evolution and has accumulated
a lot of cruft in the way it is deployed. We can simplify deployment
down to a short Cloud Build script and go back to regular App Engine
for execution.

Also remove the prod-vs-local distinction and the build tag complexity.

App Engine Flex doesn't let us have extant versions without at least
a couple dedicated VMs, and then it also imposes a limit of 20 VMs,
which makes it unsuitable for continuous deployment, where we can
rack up many versions in a short amount of time. Going back to
App Engine Standard is a better fit, since versions that aren't getting
traffic scale down to zero.

Using App Engine Standard also matches blog and go.dev,
which will help with the eventual merging of all these servers.

Change-Id: I35167b569327ad253b9d367d747072a269205b20
Reviewed-on: https://go-review.googlesource.com/c/website/+/323892
Trust: Russ Cox <rsc@golang.org>
Reviewed-by: Dmitri Shuralyov <dmitshur@golang.org>
diff --git a/cmd/golangorg/Dockerfile.prod b/cmd/golangorg/Dockerfile.prod
deleted file mode 100644
index e60c99e..0000000
--- a/cmd/golangorg/Dockerfile.prod
+++ /dev/null
@@ -1,55 +0,0 @@
-# Builder
-#########
-
-FROM golang:1.12 AS build
-
-# Check out the desired version of Go, both to build the golangorg binary and serve
-# as the goroot for content serving.
-ARG GO_REF
-RUN test -n "$GO_REF" # GO_REF is required.
-RUN git clone --single-branch --depth=1 -b $GO_REF https://go.googlesource.com/go /goroot
-RUN cd /goroot/src && ./make.bash
-
-ENV GOROOT /goroot
-ENV PATH=/goroot/bin:$PATH
-ENV GO111MODULE=on
-ENV GOPROXY=https://proxy.golang.org
-
-RUN go version
-
-COPY . /website
-
-WORKDIR /website/cmd/golangorg
-
-RUN go build -o /golangorg -tags=prod golang.org/x/website/cmd/golangorg
-
-# Clean up goroot for the final image.
-RUN cd /goroot && git clean -xdf
-
-# Add build metadata.
-RUN cd /goroot && echo "go repo HEAD: $(git rev-parse HEAD)" >> /goroot/buildinfo
-RUN echo "requested go ref: ${GO_REF}" >> /goroot/buildinfo
-ARG WEBSITE_HEAD
-RUN echo "x/website HEAD: ${WEBSITE_HEAD}" >> /goroot/buildinfo
-ARG WEBSITE_CLEAN
-RUN echo "x/website clean: ${WEBSITE_CLEAN}" >> /goroot/buildinfo
-ARG DOCKER_TAG
-RUN echo "image: ${DOCKER_TAG}" >> /goroot/buildinfo
-ARG BUILD_ENV
-RUN echo "build env: ${BUILD_ENV}" >> /goroot/buildinfo
-
-RUN rm -rf /goroot/.git
-
-# Final image
-#############
-
-FROM gcr.io/distroless/base
-
-WORKDIR /app
-COPY --from=build /golangorg /app/
-COPY --from=build /website/cmd/golangorg/hg-git-mapping.bin /app/
-
-COPY --from=build /goroot /goroot
-ENV GOROOT /goroot
-
-CMD ["/app/golangorg"]
diff --git a/cmd/golangorg/Makefile b/cmd/golangorg/Makefile
deleted file mode 100644
index 47dc4a3..0000000
--- a/cmd/golangorg/Makefile
+++ /dev/null
@@ -1,78 +0,0 @@
-# Copyright 2018 The Go Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style
-# license that can be found in the LICENSE file.
-
-.PHONY: usage
-
-GO_REF ?= release-branch.go1.16
-WEBSITE_HEAD := $(shell git rev-parse HEAD)
-WEBSITE_CLEAN := $(shell (git status --porcelain | grep -q .) && echo dirty || echo clean)
-ifeq ($(WEBSITE_CLEAN),clean)
-	DOCKER_VERSION ?= $(WEBSITE_HEAD)
-else
-	DOCKER_VERSION ?= $(WEBSITE_HEAD)-dirty
-endif
-GCP_PROJECT := golang-org
-GCP_SERVICE := default
-DOCKER_TAG := gcr.io/$(GCP_PROJECT)/golangorg:$(DOCKER_VERSION)
-
-usage:
-	@echo "See Makefile and README.md"
-	@exit 1
-
-cloud-build: Dockerfile.prod
-	gcloud builds submit \
-		--project=$(GCP_PROJECT) \
-		--config=cloudbuild.yaml \
-		--substitutions=_GO_REF=$(GO_REF),_WEBSITE_HEAD=$(WEBSITE_HEAD),_WEBSITE_CLEAN=$(WEBSITE_CLEAN),_DOCKER_TAG=$(DOCKER_TAG) \
-		../.. # source code
-
-docker-build: Dockerfile.prod
-	# NOTE(cbro): move up in directory to include entire tools repo.
-	# NOTE(cbro): any changes made to this command must also be made in cloudbuild.yaml.
-	cd ../..; docker build \
-		-f=cmd/golangorg/Dockerfile.prod \
-		--build-arg=GO_REF=$(GO_REF) \
-		--build-arg=WEBSITE_HEAD=$(WEBSITE_HEAD) \
-		--build-arg=WEBSITE_CLEAN=$(WEBSITE_CLEAN) \
-		--build-arg=DOCKER_TAG=$(DOCKER_TAG) \
-		--build-arg=BUILD_ENV=local \
-		--tag=$(DOCKER_TAG) \
-		.
-
-docker-push: docker-build
-	docker push $(DOCKER_TAG)
-
-deploy:
-	gcloud -q app deploy app.prod.yaml \
-		--project=$(GCP_PROJECT) \
-		--no-promote \
-		--image-url=$(DOCKER_TAG)
-
-get-latest-url:
-	@gcloud app versions list \
-		--service=$(GCP_SERVICE) \
-		--project=$(GCP_PROJECT) \
-		--sort-by=~version.createTime \
-		--format='value(version.versionUrl)' \
-		--limit 1 | cut -f1 # NOTE(cbro): gcloud prints out createTime as the second field.
-
-get-latest-id:
-	@gcloud app versions list \
-		--service=$(GCP_SERVICE) \
-		--project=$(GCP_PROJECT) \
-		--sort-by=~version.createTime \
-		--format='value(version.id)' \
-		--limit 1 | cut -f1 # NOTE(cbro): gcloud prints out createTime as the second field.
-
-regtest:
-	go test -v \
-		-regtest.host=$(shell make get-latest-url) \
-		-run=Live
-
-publish: regtest
-	gcloud -q app services set-traffic $(GCP_SERVICE) \
-		--splits=$(shell make get-latest-id)=1 \
-		--project=$(GCP_PROJECT)
-
-	@echo Continue with the rest of the steps in \"Deploying to golang.org\" in the README.
diff --git a/cmd/golangorg/README.md b/cmd/golangorg/README.md
index b3bc44d..e62db90 100644
--- a/cmd/golangorg/README.md
+++ b/cmd/golangorg/README.md
@@ -6,77 +6,14 @@
 
 	go run .
 
-## Local Production Mode
-
-To run in production mode locally, you need:
-
-  * the Google Cloud SDK; see https://cloud.google.com/sdk/
-  * Redis
-  * Go sources under $GOROOT
-
-Run with the `prod` tag:
-
-	go run -tags prod .
-
-In production mode it serves on localhost:8080 (not 6060).
-The port is controlled by $PORT, as in:
-
-	PORT=8081 ./golangorg
-
-## Local Production Mode using Docker
-
-To run in production mode locally using Docker, build the app's Docker container:
-
-	make docker-build
-
-Make sure redis is running on port 6379:
-
-	$ echo PING | nc localhost 6379
-	+PONG
-	^C
-
-Run the datastore emulator:
-
-	gcloud beta emulators datastore start --project golang-org
-
-In another terminal window, run the container:
-
-	$(gcloud beta emulators datastore env-init)
-
-	docker run --rm \
-		--net host \
-		--env GOLANGORG_REDIS_ADDR=localhost:6379 \
-		--env DATASTORE_EMULATOR_HOST=$DATASTORE_EMULATOR_HOST \
-		--env DATASTORE_PROJECT_ID=$DATASTORE_PROJECT_ID \
-		gcr.io/golang-org/golangorg
-
-It serves on localhost:8080.
-
 ## Deploying to golang.org
 
-1.	Run `make cloud-build deploy` to build the image, push it to gcr.io,
-	and deploy to Flex (but not yet update golang.org to point to it).
+Each time a CL is reviewed and submitted, the site is automatically deployed to App Engine.
+If the CL is submitted with a Website-Publish +1 vote,
+the new deployment automatically becomes https://golang.org/.
+Otherwise, the new deployment can be found in the
+[App Engine versions list](https://console.cloud.google.com/appengine/versions?project=golang-org&serviceId=default) and verified and manually promoted.
 
-2.	Check that the new version, mentioned on "target url:" line, looks OK.
-
-3.	If all is well, run `make publish` to publish the new version to golang.org.
-	It will run regression tests and then point the load balancer to the newly
-	deployed version.
-
-4.	Stop and/or delete any very old versions. (Stopped versions can be re-started.)
-	Keep at least one older verson to roll back to, just in case.
-
-	You can view, stop/delete, or migrate traffic between versions via the
-	[GCP Console UI](https://console.cloud.google.com/appengine/versions?project=golang-org&serviceId=default&pageState=(%22versionsTable%22:(%22f%22:%22%255B%257B_22k_22_3A_22Environment_22_2C_22t_22_3A10_2C_22v_22_3A_22_5C_22Flexible_5C_22_22_2C_22s_22_3Atrue_2C_22i_22_3A_22env_22%257D%255D%22))).
-
-5.	You're done.
-
-## Troubleshooting
-
-Ensure the Cloud SDK is on your PATH and you have the app-engine-go component
-installed (`gcloud components install app-engine-go`) and your components are
-up-to-date (`gcloud components update`).
-
-For deployment, make sure you're signed in to gcloud:
-
-	gcloud auth login
+If the automatic deployment is not working, or to check on the status of a pending deployment,
+see the “website-redeploy-golang-org” trigger in the
+[Cloud Build console](https://console.cloud.google.com/cloud-build/builds?project=golang-org).
diff --git a/cmd/golangorg/app.dev.yaml b/cmd/golangorg/app.dev.yaml
deleted file mode 100644
index 0f8f1cc..0000000
--- a/cmd/golangorg/app.dev.yaml
+++ /dev/null
@@ -1,13 +0,0 @@
-runtime: go
-api_version: go1
-instance_class: F4_1G
-
-handlers:
-- url: /s
-  script: _go_app
-  login: admin
-- url: /dl/init
-  script: _go_app
-  login: admin
-- url: /.*
-  script: _go_app
diff --git a/cmd/golangorg/app.prod.yaml b/cmd/golangorg/app.prod.yaml
deleted file mode 100644
index 1ffe73e..0000000
--- a/cmd/golangorg/app.prod.yaml
+++ /dev/null
@@ -1,23 +0,0 @@
-service: default
-runtime: custom
-env: flex
-
-env_variables:
-  GOLANGORG_CHECK_COUNTRY: true
-  GOLANGORG_REQUIRE_DL_SECRET_KEY: true
-  GOLANGORG_ENFORCE_HOSTS: true
-  GOLANGORG_REDIS_ADDR: 10.0.0.4:6379 # instance "gophercache"
-  GOLANGORG_ANALYTICS: UA-11222381-2
-  DATASTORE_PROJECT_ID: golang-org
-
-network:
-  name: golang
-
-resources:
-  cpu: 4
-  memory_gb: 7.50
-
-liveness_check:
-  path: /_ah/health
-readiness_check:
-  path: /_ah/health
diff --git a/cmd/golangorg/app.yaml b/cmd/golangorg/app.yaml
new file mode 100644
index 0000000..c03697d
--- /dev/null
+++ b/cmd/golangorg/app.yaml
@@ -0,0 +1,23 @@
+# This app is deployed via Cloud Build as directed by cloudbuild.yaml.
+# Do not deploy directly.
+
+service: default
+runtime: go115
+main: ./cmd/golangorg
+
+env_variables:
+  GOLANGORG_CHECK_COUNTRY: true
+  GOLANGORG_REQUIRE_DL_SECRET_KEY: true
+  GOLANGORG_ENFORCE_HOSTS: true
+  GOLANGORG_REDIS_ADDR: 10.0.0.4:6379 # instance "gophercache"
+  GOLANGORG_ANALYTICS: UA-11222381-2
+  DATASTORE_PROJECT_ID: golang-org
+
+# For access to our Redis instance.
+vpc_access_connector:
+  name: projects/golang-org/locations/us-central1/connectors/golang-vpc-connector
+
+handlers:
+- url: /.*
+  script: auto
+  secure: always
diff --git a/cmd/golangorg/cloudbuild.yaml b/cmd/golangorg/cloudbuild.yaml
index af6e8a2..3b68aee 100644
--- a/cmd/golangorg/cloudbuild.yaml
+++ b/cmd/golangorg/cloudbuild.yaml
@@ -1,25 +1,31 @@
-# Copyright 2018 The Go Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style
-# license that can be found in the LICENSE file.
+# This Cloud Build file is run automatically when commits land in the website repo.
+# See https://console.cloud.google.com/cloud-build/triggers?project=golang-org.
+# Do not run directly.
 
-# NOTE(cbro): any changes to the docker command must also be
-# made in docker-build in the Makefile.
-#
-# Variable substitutions must have a preceding underscore. See:
-# https://cloud.google.com/cloud-build/docs/configuring-builds/substitute-variable-values#using_user-defined_substitutions
 steps:
-- name: 'gcr.io/cloud-builders/docker'
-  args: [
-    'build',
-    '-f=cmd/golangorg/Dockerfile.prod',
-    '--build-arg=GO_REF=${_GO_REF}',
-    '--build-arg=WEBSITE_HEAD=${_WEBSITE_HEAD}',
-    '--build-arg=WEBSITE_CLEAN=${_WEBSITE_CLEAN}',
-    '--build-arg=DOCKER_TAG=${_DOCKER_TAG}',
-    '--build-arg=BUILD_ENV=cloudbuild',
-    '--tag=${_DOCKER_TAG}',
-    '.',
-  ]
-images: ['${_DOCKER_TAG}']
+  - name: gcr.io/cloud-builders/git
+    args: [
+      "clone", "--branch=${_GO_REF}", "--depth=1",
+      "https://go.googlesource.com/go", "_gotmp",
+    ]
+  - name: gcr.io/cloud-builders/git
+    dir: _gotmp
+    args: [
+      "archive", "--format=zip", "--output=../_goroot.zip", "HEAD",
+    ]
+  - name: mirror.gcr.io/library/golang
+    args: ["rm", "-rf", "_gotmp"]
+  - name: mirror.gcr.io/library/golang
+    args: ["go", "test", "./..."]
+  - name: gcr.io/cloud-builders/gcloud
+    entrypoint: bash
+    args: ["./go-app-deploy.sh", "cmd/golangorg/app.yaml"]
+  - name: mirror.gcr.io/library/golang
+    args: [
+      "go", "run", "./cmd/versionprune", "--dry_run=false",
+      "--project=$PROJECT_ID", "--service=default",
+    ]
+    dir: go.dev
+
 options:
-  machineType: 'N1_HIGHCPU_8' # building the godoc index takes a lot of memory.
+  machineType: N1_HIGHCPU_8
diff --git a/cmd/golangorg/godoc_test.go b/cmd/golangorg/godoc_test.go
index 66514a1..79d0201 100644
--- a/cmd/golangorg/godoc_test.go
+++ b/cmd/golangorg/godoc_test.go
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-package main_test
+package main
 
 import (
 	"bytes"
@@ -12,49 +12,12 @@
 	"net/http"
 	"os"
 	"os/exec"
-	"path/filepath"
-	"runtime"
 	"testing"
 	"time"
 
 	"golang.org/x/website/internal/webtest"
 )
 
-// buildGodoc builds the godoc executable.
-// It returns its path, and a cleanup function.
-//
-// TODO(adonovan): opt: do this at most once, and do the cleanup
-// exactly once.  How though?  There's no atexit.
-func buildGodoc(t *testing.T) (bin string, cleanup func()) {
-	if runtime.GOARCH == "arm" {
-		t.Skip("skipping test on arm platforms; too slow")
-	}
-	if _, err := exec.LookPath("go"); err != nil {
-		t.Skipf("skipping test because 'go' command unavailable: %v", err)
-	}
-
-	tmp, err := ioutil.TempDir("", "godoc-regtest-")
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer func() {
-		if cleanup == nil { // probably, go build failed.
-			os.RemoveAll(tmp)
-		}
-	}()
-
-	bin = filepath.Join(tmp, "godoc")
-	if runtime.GOOS == "windows" {
-		bin += ".exe"
-	}
-	cmd := exec.Command("go", "build", "-o", bin)
-	if err := cmd.Run(); err != nil {
-		t.Fatalf("Building godoc: %v", err)
-	}
-
-	return bin, func() { os.RemoveAll(tmp) }
-}
-
 func serverAddress(t *testing.T) string {
 	ln, err := net.Listen("tcp", "127.0.0.1:0")
 	if err != nil {
@@ -101,21 +64,21 @@
 	cmd.Wait()
 }
 
-// Basic integration test for godoc HTTP interface.
-func TestWeb(t *testing.T) {
-	testWeb(t)
+func init() {
+	// TestWeb reinvokes the test binary (us) with -be-main
+	// to simulate running the actual golangorg binary.
+	if len(os.Args) >= 2 && os.Args[1] == "-be-main" {
+		os.Args = os.Args[1:]
+		os.Args[0] = "(golangorg)"
+		main()
+		os.Exit(0)
+	}
 }
 
 // Basic integration test for godoc HTTP interface.
-func testWeb(t *testing.T) {
-	if runtime.GOOS == "plan9" {
-		t.Skip("skipping on plan9; fails to start up quickly enough")
-	}
-	bin, cleanup := buildGodoc(t)
-	defer cleanup()
+func TestWeb(t *testing.T) {
 	addr := serverAddress(t)
-	args := []string{fmt.Sprintf("-http=%s", addr)}
-	cmd := exec.Command(bin, args...)
+	cmd := exec.Command(os.Args[0], "-be-main", "-http="+addr)
 	cmd.Stdout = os.Stderr
 	cmd.Stderr = os.Stderr
 	cmd.Args[0] = "godoc"
diff --git a/cmd/golangorg/local.go b/cmd/golangorg/local.go
deleted file mode 100644
index 3e0d9ab..0000000
--- a/cmd/golangorg/local.go
+++ /dev/null
@@ -1,40 +0,0 @@
-// Copyright 2014 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-//go:build !prod
-// +build !prod
-
-package main
-
-import (
-	"fmt"
-	"log"
-	"net/http"
-	"os"
-	"path/filepath"
-	"runtime"
-
-	// This package registers "/compile" and "/share" handlers
-	// that redirect to the golang.org playground.
-	_ "golang.org/x/tools/playground"
-)
-
-func earlySetup() {
-	_, file, _, ok := runtime.Caller(0)
-	if !ok {
-		fmt.Fprintln(os.Stderr, "runtime.Caller failed: cannot find templates for -a mode.")
-		os.Exit(2)
-	}
-	dir := filepath.Join(file, "../../../_content")
-	if _, err := os.Stat(filepath.Join(dir, "lib/godoc/site.html")); err != nil {
-		log.Printf("warning: cannot find template dir; using embedded copy")
-		return
-	}
-	*templateDir = dir
-}
-
-func lateSetup(mux *http.ServeMux) {
-	// Register a redirect handler for /dl/ to the golang.org download page.
-	mux.Handle("/dl/", http.RedirectHandler("https://golang.org/dl/", http.StatusFound))
-}
diff --git a/cmd/golangorg/main.go b/cmd/golangorg/main.go
index d0beec5..19cb8d5 100644
--- a/cmd/golangorg/main.go
+++ b/cmd/golangorg/main.go
@@ -18,24 +18,42 @@
 package main
 
 import (
+	"context"
 	"flag"
 	"fmt"
+	"io"
 	"log"
 	"net/http"
 	"os"
+	"path/filepath"
 	"runtime"
+	"strings"
 
+	"cloud.google.com/go/datastore"
 	"golang.org/x/website"
+	"golang.org/x/website/internal/backport/archive/zip"
 	"golang.org/x/website/internal/backport/io/fs"
 	"golang.org/x/website/internal/backport/osfs"
+	"golang.org/x/website/internal/dl"
+	"golang.org/x/website/internal/memcache"
+	"golang.org/x/website/internal/proxy"
+	"golang.org/x/website/internal/redirect"
+	"golang.org/x/website/internal/short"
 	"golang.org/x/website/internal/web"
+
+	// Registers "/compile" handler that redirects to play.golang.org/compile.
+	// If we are in prod we will register "golang.org/compile" separately,
+	// which will get used instead.
+	_ "golang.org/x/tools/playground"
 )
 
 var (
-	httpAddr    = flag.String("http", "localhost:6060", "HTTP service address")
-	verbose     = flag.Bool("v", false, "verbose mode")
-	goroot      = flag.String("goroot", runtime.GOROOT(), "Go root directory")
-	templateDir = flag.String("templates", "", "load templates/JS/CSS from disk in this directory (usually /path-to-website/content)")
+	httpAddr   = flag.String("http", "localhost:6060", "HTTP service address")
+	verbose    = flag.Bool("v", false, "verbose mode")
+	goroot     = flag.String("goroot", runtime.GOROOT(), "Go root directory")
+	contentDir = flag.String("content", "", "path to _content directory")
+
+	runningOnAppEngine = os.Getenv("PORT") != ""
 )
 
 func usage() {
@@ -52,7 +70,25 @@
 }
 
 func main() {
-	earlySetup()
+	repoRoot := "../.."
+	if _, err := os.Stat("_content"); err == nil {
+		repoRoot = "."
+	}
+
+	if runningOnAppEngine {
+		log.Print("golang.org server starting")
+		*goroot = "_goroot.zip"
+		log.SetFlags(log.Lshortfile | log.LstdFlags)
+		port := "8080"
+		if p := os.Getenv("PORT"); p != "" {
+			port = p
+		}
+		*httpAddr = ":" + port
+	} else {
+		if *contentDir == "" {
+			*contentDir = filepath.Join(repoRoot, "_content")
+		}
+	}
 
 	flag.Usage = usage
 	flag.Parse()
@@ -69,22 +105,48 @@
 
 	// Serve files from _content, falling back to GOROOT.
 	var content fs.FS
-	if *templateDir != "" {
-		content = osfs.DirFS(*templateDir)
+	if *contentDir != "" {
+		content = osfs.DirFS(*contentDir)
 	} else {
 		content = website.Content
 	}
-	fsys = unionFS{content, osfs.DirFS(*goroot)}
+
+	var gorootFS fs.FS
+	if strings.HasSuffix(*goroot, ".zip") {
+		z, err := zip.OpenReader(*goroot)
+		if err != nil {
+			log.Fatal(err)
+		}
+		defer z.Close()
+		gorootFS = z
+	} else {
+		gorootFS = osfs.DirFS(*goroot)
+	}
+	fsys = unionFS{content, gorootFS}
 
 	var err error
 	site, err = web.NewSite(fsys)
 	if err != nil {
-		log.Fatal(err)
+		log.Fatalf("NewSite: %v", err)
 	}
 	site.GoogleCN = googleCN
 
 	mux := registerHandlers(site)
-	lateSetup(mux)
+
+	http.HandleFunc("/robots.txt", func(w http.ResponseWriter, r *http.Request) {
+		io.WriteString(w, "User-agent: *\nDisallow: /search\n")
+	})
+
+	if err := redirect.LoadChangeMap(filepath.Join(repoRoot, "cmd/golangorg/hg-git-mapping.bin")); err != nil {
+		log.Fatalf("LoadChangeMap: %v", err)
+	}
+
+	if runningOnAppEngine {
+		appEngineSetup(mux)
+	} else {
+		// Register a redirect handler for /dl/ to the golang.org download page.
+		mux.Handle("/dl/", http.RedirectHandler("https://golang.org/dl/", http.StatusFound))
+	}
 
 	var handler http.Handler = http.DefaultServeMux
 	if *verbose {
@@ -163,3 +225,33 @@
 	}
 	return nil, errOut
 }
+
+func appEngineSetup(mux *http.ServeMux) {
+	site.GoogleAnalytics = os.Getenv("GOLANGORG_ANALYTICS")
+
+	ctx := context.Background()
+
+	datastoreClient, err := datastore.NewClient(ctx, "")
+	if err != nil {
+		if strings.Contains(err.Error(), "missing project") {
+			log.Fatalf("Missing datastore project. Set the DATASTORE_PROJECT_ID env variable. Use `gcloud beta emulators datastore` to start a local datastore.")
+		}
+		log.Fatalf("datastore.NewClient: %v.", err)
+	}
+
+	redisAddr := os.Getenv("GOLANGORG_REDIS_ADDR")
+	if redisAddr == "" {
+		log.Fatalf("Missing redis server for golangorg in production mode. set GOLANGORG_REDIS_ADDR environment variable.")
+	}
+	memcacheClient := memcache.New(redisAddr)
+
+	dl.RegisterHandlers(mux, site, datastoreClient, memcacheClient)
+	short.RegisterHandlers(mux, datastoreClient, memcacheClient)
+
+	// Register /compile and /share handlers against the default serve mux
+	// so that other app modules can make plain HTTP requests to those
+	// hosts. (For reasons, HTTPS communication between modules is broken.)
+	proxy.RegisterHandlers(http.DefaultServeMux)
+
+	log.Println("AppEngine initialization complete")
+}
diff --git a/cmd/golangorg/prod.go b/cmd/golangorg/prod.go
deleted file mode 100644
index bc81910..0000000
--- a/cmd/golangorg/prod.go
+++ /dev/null
@@ -1,83 +0,0 @@
-// Copyright 2011 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-//go:build prod
-// +build prod
-
-package main
-
-import (
-	"context"
-	"io"
-	"log"
-	"net/http"
-	"os"
-	"strings"
-
-	"golang.org/x/website/internal/dl"
-	"golang.org/x/website/internal/proxy"
-	"golang.org/x/website/internal/redirect"
-	"golang.org/x/website/internal/short"
-
-	"cloud.google.com/go/datastore"
-	"golang.org/x/website/internal/memcache"
-)
-
-func earlySetup() {
-	log.SetFlags(log.Lshortfile | log.LstdFlags)
-	log.Println("initializing golang.org server ...")
-
-	port := "8080"
-	if p := os.Getenv("PORT"); p != "" {
-		port = p
-	}
-	*httpAddr = ":" + port
-}
-
-func lateSetup(mux *http.ServeMux) {
-	site.GoogleAnalytics = os.Getenv("GOLANGORG_ANALYTICS")
-
-	datastoreClient, memcacheClient := getClients()
-
-	dl.RegisterHandlers(mux, site, datastoreClient, memcacheClient)
-	short.RegisterHandlers(mux, datastoreClient, memcacheClient)
-
-	// Register /compile and /share handlers against the default serve mux
-	// so that other app modules can make plain HTTP requests to those
-	// hosts. (For reasons, HTTPS communication between modules is broken.)
-	proxy.RegisterHandlers(http.DefaultServeMux)
-
-	http.HandleFunc("/_ah/health", func(w http.ResponseWriter, r *http.Request) {
-		io.WriteString(w, "ok")
-	})
-
-	http.HandleFunc("/robots.txt", func(w http.ResponseWriter, r *http.Request) {
-		io.WriteString(w, "User-agent: *\nDisallow: /search\n")
-	})
-
-	if err := redirect.LoadChangeMap("hg-git-mapping.bin"); err != nil {
-		log.Fatalf("LoadChangeMap: %v", err)
-	}
-
-	log.Println("godoc initialization complete")
-}
-
-func getClients() (*datastore.Client, *memcache.Client) {
-	ctx := context.Background()
-
-	datastoreClient, err := datastore.NewClient(ctx, "")
-	if err != nil {
-		if strings.Contains(err.Error(), "missing project") {
-			log.Fatalf("Missing datastore project. Set the DATASTORE_PROJECT_ID env variable. Use `gcloud beta emulators datastore` to start a local datastore.")
-		}
-		log.Fatalf("datastore.NewClient: %v.", err)
-	}
-
-	redisAddr := os.Getenv("GOLANGORG_REDIS_ADDR")
-	if redisAddr == "" {
-		log.Fatalf("Missing redis server for golangorg in production mode. set GOLANGORG_REDIS_ADDR environment variable.")
-	}
-	memcacheClient := memcache.New(redisAddr)
-	return datastoreClient, memcacheClient
-}
diff --git a/go-app-deploy.sh b/go-app-deploy.sh
index cc80e36..17d036c 100644
--- a/go-app-deploy.sh
+++ b/go-app-deploy.sh
@@ -39,5 +39,4 @@
 	git log -n1 --date='format:%Y-%m-%d-%H%M%S' --pretty='format:%cd-%h'
 )
 
-gcloud app deploy $promote -v $version
-
+gcloud app deploy $promote -v $version "$@"
diff --git a/internal/proxy/proxy.go b/internal/proxy/proxy.go
index cc740d6..2843a77 100644
--- a/internal/proxy/proxy.go
+++ b/internal/proxy/proxy.go
@@ -42,8 +42,8 @@
 var cacheControlHeader = fmt.Sprintf("public, max-age=%d", int(expires.Seconds()))
 
 func RegisterHandlers(mux *http.ServeMux) {
-	mux.HandleFunc("/compile", compile)
-	mux.HandleFunc("/share", share)
+	mux.HandleFunc("golang.org/compile", compile)
+	mux.HandleFunc("golang.org/share", share)
 }
 
 func compile(w http.ResponseWriter, r *http.Request) {