Lesson 10 of 28
Module 3 · Task — Ship a Go app through GitHub Actions to a cluster (via Claude)
The task
Build a small Go HTTP service in a GitHub repo. Drive Claude to produce a GitHub Actions workflow that, on every push to main, builds + tests, pushes the image to GHCR tagged with the commit SHA, spins up a kind cluster inside the runner, installs your Helm chart from module 2 pointing at that image, and smoke-tests /healthz. You read every workflow step before you push.
Acceptance test: push a commit to main. The workflow goes green. The image exists at ghcr.io/<your-username>/hello-devops:<sha> and shows up in your repo's Packages tab.
Setup
- Module 2 completed — you have a working Helm chart at
./podinfo-chart(you'll copy it in). - A new empty GitHub repo called
hello-devops. In Settings → Actions → General → Workflow permissions, enable "Read and write permissions". Without this, the defaultGITHUB_TOKENcan't push to GHCR.
Drive it through Claude
Bootstrap the Go app. Send Claude:
"In a fresh clone of my
hello-devopsrepo, initialise a Go module atgithub.com/<my-username>/hello-devops. Writemain.gowith an HTTP server on port 9898 exposing/(returns{\"hello\":\"devops\"}) and/healthz(returns{\"status\":\"OK\"}). Also writemain_test.gothat tests/healthzreturns 200 withOKin the body. Also write a multi-stageDockerfilewith agolang:1.22-alpinebuild stage and a distroless runtime stage, EXPOSE 9898, non-root user."Open every file Claude creates. Read
main.go,main_test.go, and theDockerfileend-to-end. Ask Claude: why a distroless base image? why is running as non-root important for production? Answer in your own words before moving on.Bring the Helm chart in. Send:
"Copy my
podinfo-chartdirectory from module 2 into this repo, rename it tochart/, and change the defaultimage.repositoryinvalues.yamltoghcr.io/<my-username>/hello-devops."Write the workflow. Send:
"Write
.github/workflows/deploy.ymlwith a single job calledpipelineonubuntu-latest. Steps: (1) checkout, (2) setup-go 1.22, (3)go vet ./...andgo test ./... -count=1, (4) docker/setup-buildx-action, (5) login to GHCR with theGITHUB_TOKEN, (6) build and push withdocker/build-push-actiontagging both${{ github.sha }}andlatest, (7) start akindcluster withhelm/kind-action, (8)kubectl create namespace demoandhelm install hello ./chart -n demo --set image.repository=$IMAGE --set image.tag=${{ github.sha }} --wait --timeout=5m, (9)kubectl -n demo port-forward svc/hello-chart 9898:9898 &, sleep 3, thencurl --fail http://localhost:9898/healthz. Setpermissions: contents: read, packages: writeat the job level.IMAGEisghcr.io/${{ github.repository }}."Read the generated workflow line by line. Ask Claude: why is
packages: writea narrowly-scoped permission rather than using a personal access token? what would break if I removed it?Commit + push. Send:
"Commit everything and push to
main. Then open the Actions tab URL for me."
A note on identity — you just did real IAM
The workflow you wrote uses secrets.GITHUB_TOKEN, an ephemeral, repo-scoped token that GitHub mints for each run. It expires when the workflow ends. That's the modern pattern — and it's a form of identity federation: your workflow's identity is "this specific run of this repo", not a long-lived credential.
When you deploy to a real cloud (GKE, EKS, AKS), the same principle applies via OIDC federation: GitHub Actions issues a short-lived OIDC token, your cloud trusts GitHub's issuer, and a specific repo/branch combination is mapped to a cloud IAM role. The workflow assumes the role for the duration of the run. No static service-account keys in repo secrets.
The anti-pattern is what this replaces: a GCP_SA_KEY secret in the repo settings that rotates by hand once a year and leaks the first time anyone forks. Module 7 goes deep on this. For now, notice: you already did the right thing for GHCR. Remember the pattern when you wire a cloud deploy.
Break it on purpose
- Remove
packages: writefrom thepermissions:block indeploy.yml. Commit + push. - Predict: which step will fail, and with what kind of error message? Write it down.
- Watch the run. Read the error.
- Restore
packages: writeand push again. Confirm green.
The failure mode: CI permissions are themselves configuration. A missing permission is a silent foot-gun — the workflow is syntactically correct, the test passes, the build passes, then the push dies with denied. Your skill will want to know this class of failure.
Acceptance test
Push a commit to main. After ~2–4 minutes (longer cold), the run is green. Then:
- Repo main page → Packages (right sidebar) shows
hello-devops. - Click into it. You see a tag matching your commit SHA plus
latest.
If you want to verify you can pull it locally (optional):
echo $CR_PAT | docker login ghcr.io -u <your-username> --password-stdin
docker pull ghcr.io/<your-username>/hello-devops:latest
docker run --rm -p 9898:9898 ghcr.io/<your-username>/hello-devops:latest
CR_PAT is a PAT with read:packages. The CI smoke test already proved the image works.
What to keep for the next lesson
Keep the repo URL, the workflow file, and your Break it on purpose note on the missing permission. In the next lesson you'll codify .claude/skills/github-actions-k8s-deploy/ and teach the skill to start a new workflow with the correct permissions: block rather than waiting for the first push to fail.