Skip to content

Commit 21ccbb3

Browse files
ci(deploy): auto-deploy on push to master (#32)
A worker fix shipped to master but never deployed because someone had to run `docker buildx build && kubectl set image` by hand. A user got the same broken expiry email twice as a result. Close that gap — for the worker repo, that's literally how the bug happened. On every push to master this workflow now: 1. checks out worker + sibling common/ + proto/ to match Dockerfile 2. runs `go test ./... -short -count=1` (fails the job on red tests) 3. builds linux/amd64 with GIT_SHA/BUILD_TIME/VERSION build-args 4. pushes ghcr.io/mastermanas805/instant-worker:<master-SHA> + :latest 5. kubectl set image deployment/instant-worker + rollout status (180s) 6. verifies the deployment now points at the exact tag we built 7. shells into the new pod and curls localhost:8091/healthz to confirm the binary reports the new commit_id (best-effort; the prod image is distroless and may not have curl/wget, in which case the image-tag check above is the load-bearing gate) Operator action: add KUBECONFIG_B64 to repo secrets (base64-encoded kubeconfig). Without it the kubeconfig step fails fast with a clear error message. Concurrency group `deploy-Deploy` with cancel-in-progress=false so two merges in a row queue instead of racing. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent edf0c47 commit 21ccbb3

1 file changed

Lines changed: 194 additions & 0 deletions

File tree

.github/workflows/deploy.yml

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
# instant.dev/worker — Auto-deploy on push to master
2+
#
3+
# Why this exists:
4+
# On 2026-05-15, a worker code fix shipped to master but was never
5+
# deployed — an operator had to manually `docker buildx build &&
6+
# kubectl set image`. A user received the same broken expiry email
7+
# twice as a result. This workflow eliminates that gap.
8+
#
9+
# Build context note:
10+
# The Dockerfile expects to be invoked from the parent of worker/, with
11+
# sibling common/ and proto/ directories present. In CI we mirror that
12+
# by checking out:
13+
# . (workspace root)
14+
# ├── worker/ (this repo)
15+
# ├── common/ (sibling repo)
16+
# └── proto/ (sibling repo)
17+
# then `docker buildx build -f worker/Dockerfile .` from the workspace root.
18+
#
19+
# Required repo secret:
20+
# KUBECONFIG_B64 — base64-encoded kubeconfig with permission to
21+
# `kubectl set image deployment/instant-worker -n instant-infra`.
22+
#
23+
# GHCR auth uses the per-job GITHUB_TOKEN with `packages: write`.
24+
25+
name: Deploy
26+
27+
on:
28+
push:
29+
branches: [master]
30+
workflow_dispatch:
31+
32+
concurrency:
33+
group: deploy-${{ github.workflow }}
34+
cancel-in-progress: false
35+
36+
permissions:
37+
contents: read
38+
packages: write
39+
40+
env:
41+
IMAGE_REPO: ghcr.io/mastermanas805/instant-worker
42+
K8S_NAMESPACE: instant-infra
43+
K8S_DEPLOYMENT: instant-worker
44+
K8S_CONTAINER: worker
45+
46+
jobs:
47+
deploy:
48+
runs-on: ubuntu-latest
49+
steps:
50+
- name: Checkout worker (this repo) into ./worker
51+
uses: actions/checkout@v4
52+
with:
53+
path: worker
54+
55+
- name: Checkout common sibling into ./common
56+
uses: actions/checkout@v4
57+
with:
58+
repository: ${{ vars.COMMON_REPO || format('{0}/common', github.repository_owner) }}
59+
token: ${{ secrets.GITHUB_TOKEN }}
60+
path: common
61+
62+
- name: Checkout proto sibling into ./proto
63+
uses: actions/checkout@v4
64+
with:
65+
repository: ${{ vars.PROTO_REPO || format('{0}/proto', github.repository_owner) }}
66+
token: ${{ secrets.GITHUB_TOKEN }}
67+
path: proto
68+
69+
- name: Compute build metadata
70+
id: meta
71+
run: |
72+
SHORT_SHA="${GITHUB_SHA:0:7}"
73+
BUILD_TIME="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
74+
VERSION="master-${SHORT_SHA}"
75+
echo "short_sha=${SHORT_SHA}" >> "$GITHUB_OUTPUT"
76+
echo "build_time=${BUILD_TIME}" >> "$GITHUB_OUTPUT"
77+
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
78+
echo "Built ${VERSION} (${BUILD_TIME})"
79+
80+
- name: Set up Go (for unit tests + go.mod replace directives)
81+
uses: actions/setup-go@v5
82+
with:
83+
go-version: '1.25'
84+
85+
- name: Run unit tests (short, no integration deps)
86+
# go.mod uses `replace instant.dev/common => ../common` and
87+
# `replace instant.dev/proto => ../proto`. When `go test` runs
88+
# inside ./worker, the relative paths resolve to ./common and
89+
# ./proto in the workspace root — already correct, no mv needed.
90+
working-directory: worker
91+
run: go test ./... -short -count=1
92+
93+
- name: Set up Docker Buildx
94+
uses: docker/setup-buildx-action@v3
95+
96+
- name: Log in to GHCR
97+
uses: docker/login-action@v3
98+
with:
99+
registry: ghcr.io
100+
username: ${{ github.actor }}
101+
password: ${{ secrets.GITHUB_TOKEN }}
102+
103+
- name: Build and push image
104+
# Build context = workspace root so Dockerfile's
105+
# `COPY proto/`, `COPY common/`, `COPY worker/` all resolve.
106+
run: |
107+
docker buildx build \
108+
--platform linux/amd64 \
109+
-f worker/Dockerfile \
110+
--build-arg GIT_SHA="${{ steps.meta.outputs.short_sha }}" \
111+
--build-arg BUILD_TIME="${{ steps.meta.outputs.build_time }}" \
112+
--build-arg VERSION="${{ steps.meta.outputs.version }}" \
113+
-t "${IMAGE_REPO}:${{ steps.meta.outputs.version }}" \
114+
-t "${IMAGE_REPO}:latest" \
115+
--push \
116+
.
117+
118+
- name: Set up kubectl
119+
uses: azure/setup-kubectl@v3
120+
with:
121+
version: 'latest'
122+
123+
- name: Configure kubeconfig from KUBECONFIG_B64 secret
124+
env:
125+
KUBECONFIG_B64: ${{ secrets.KUBECONFIG_B64 }}
126+
run: |
127+
if [ -z "${KUBECONFIG_B64}" ]; then
128+
echo "::error::KUBECONFIG_B64 repo secret is not set. Add it under Settings → Secrets → Actions."
129+
exit 1
130+
fi
131+
mkdir -p "$HOME/.kube"
132+
echo "$KUBECONFIG_B64" | base64 -d > "$HOME/.kube/config"
133+
chmod 600 "$HOME/.kube/config"
134+
kubectl version --client=true
135+
136+
- name: Roll out new image
137+
run: |
138+
IMAGE="${IMAGE_REPO}:${{ steps.meta.outputs.version }}"
139+
echo "Setting ${K8S_DEPLOYMENT}.${K8S_CONTAINER} to ${IMAGE}"
140+
kubectl set image \
141+
"deployment/${K8S_DEPLOYMENT}" \
142+
"${K8S_CONTAINER}=${IMAGE}" \
143+
-n "${K8S_NAMESPACE}"
144+
kubectl rollout status \
145+
"deployment/${K8S_DEPLOYMENT}" \
146+
-n "${K8S_NAMESPACE}" \
147+
--timeout=180s
148+
149+
- name: Verify rolled-out image tag matches built version
150+
# The worker has no public ingress, so we can't curl an external
151+
# /healthz. Instead, verify the deployment now references the
152+
# exact image tag we built — this is sufficient: rollout-status
153+
# above already confirmed the new pod is Ready, and Ready means
154+
# the container's startup probe passed.
155+
run: |
156+
ROLLED=$(kubectl get deployment "${K8S_DEPLOYMENT}" -n "${K8S_NAMESPACE}" \
157+
-o jsonpath="{.spec.template.spec.containers[?(@.name=='${K8S_CONTAINER}')].image}")
158+
EXPECTED="${IMAGE_REPO}:${{ steps.meta.outputs.version }}"
159+
echo "Live image: ${ROLLED}"
160+
echo "Expected: ${EXPECTED}"
161+
if [ "${ROLLED}" != "${EXPECTED}" ]; then
162+
echo "::error::Rolled image (${ROLLED}) != expected (${EXPECTED})"
163+
exit 1
164+
fi
165+
166+
- name: Confirm new pod reports new SHA via in-cluster /healthz
167+
# worker exposes /healthz on :8091 inside the cluster (see CLAUDE.md:
168+
# "Mirrored on provisioner-sidecar (:8092), worker-healthz (:8091)").
169+
# We shell into the freshest Ready pod and curl localhost to confirm
170+
# the binary itself reports our short SHA in commit_id.
171+
run: |
172+
SHORT_SHA="${{ steps.meta.outputs.short_sha }}"
173+
POD=$(kubectl get pod -n "${K8S_NAMESPACE}" \
174+
-l "app=${K8S_DEPLOYMENT}" \
175+
--field-selector=status.phase=Running \
176+
-o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true)
177+
if [ -z "${POD}" ]; then
178+
echo "::warning::could not locate a running ${K8S_DEPLOYMENT} pod by label app=${K8S_DEPLOYMENT}; rollout already succeeded, skipping in-pod SHA check"
179+
exit 0
180+
fi
181+
echo "Probing /healthz inside ${POD}"
182+
for i in 1 2 3 4 5; do
183+
BODY=$(kubectl exec -n "${K8S_NAMESPACE}" "${POD}" -- \
184+
sh -c 'wget -qO- http://127.0.0.1:8091/healthz 2>/dev/null || curl -fsSL http://127.0.0.1:8091/healthz 2>/dev/null' || echo "")
185+
echo "Attempt ${i}: ${BODY}"
186+
if echo "${BODY}" | grep -q "${SHORT_SHA}"; then
187+
echo "Confirmed in-pod /healthz reports commit_id=${SHORT_SHA}"
188+
exit 0
189+
fi
190+
sleep 3
191+
done
192+
# Distroless workers may not have wget/curl. Don't fail the deploy
193+
# in that case — the image-tag check above is the load-bearing gate.
194+
echo "::warning::could not confirm SHA via in-pod /healthz (distroless image likely has no curl/wget). Image-tag check passed; deploy is good."

0 commit comments

Comments
 (0)