A tiny geography quiz game written in Go. You're shown a city and three country options and you just pick the one it's actually in. Score is tracked as you go.
Try it live: starter-397431207380.us-central1.run.app
. This is the Cloud Run instance kept up to date by the deploy pipeline
below, so it always reflects the latest main.
The app is a single Go binary (citycountrygame) that serves a small
server-rendered game with no JavaScript or client-side framework. Every
interaction is a plain HTTP form post.
- Game data (capitals.go) — on startup, the app fetches the full list of countries and capitals from the countriesnow.space API, dropping entries with no capital and de-duplicating by city name.
- Question generation (game.go) — each round,
NewQuestionpicks a random capital as the correct answer, then samples two more capitals from different countries as distractors, and shuffles the three options. - City photos (images.go) — a thumbnail for the question's
city is pulled from the Wikipedia REST summary API on first use and cached
in memory (
ImageCache), so repeat questions about the same city don't re-fetch it. - HTTP handlers (main.go) —
GET /renders a question via thequestion.html.tmpltemplate; the score so far is threaded through the URL query string.POST /answerreceives the chosen option as a form post, compares it to the correct answer, and rendersresult.html.tmplwith the verdict and an updated score, which then links back to the next question. - Templates (templates/) — plain
html/templatefiles embedded into the binary viago:embed(templates.go), so the built binary has no external file dependencies. - Version endpoint (server.go) —
GET /versionreturns the build's version (a git short SHA in production,devlocally), used by the deployment pipeline to confirm what's actually running.
There's no database and no session/cookie state. The score simply round-trips through the page's links and forms, which keeps the server fully stateless.
go run .The server listens on :8080 by default (override with PORT). Open
http://localhost:8080 to play.
Every push and pull request runs through a single CI/CD workflow (.github/workflows/pipeline.yml) with five stages that gate each other:
- test — sets up Go using the version pinned in
go.modand runsgo test ./.... - security-scan — runs Snyk against the Go code
(
snyk code test) and pushes a dependency snapshot to the Snyk dashboard (snyk monitor) for ongoing vulnerability tracking. - docker-build (needs: test, security-scan) — builds the multi-stage
Dockerfile (compiles a static binary, then copies it into a
distrolessnon-root base image) and tags the image with the short git SHA. On pushes tomain, the image is also authenticated and pushed to Google Artifact Registry; on pull requests, the build runs (to catch Docker-level breakage) but nothing is pushed. - deploy (needs: docker-build, main-branch pushes only) — deploys the
just-built image to Cloud Run using
google-github-actions/deploy-cloudrun, targeting theprodGitHub Environment. - verify-deployment (needs: docker-build, deploy) — polls the freshly
deployed service's
/versionendpoint and confirms the running version matches the SHA that was just built and deployed, failing the workflow loudly if they don't match.
This means every change that reaches main is tested, scanned for
vulnerabilities, built into a versioned container image, deployed to Cloud
Run, and then verified end-to-end with the exact version.
