Skip to content

chore(deps): Bump the actions group across 1 directory with 7 updates… #55

chore(deps): Bump the actions group across 1 directory with 7 updates…

chore(deps): Bump the actions group across 1 directory with 7 updates… #55

Workflow file for this run

# instant.dev/provisioner — Auto-deploy on push to master
#
# Why this exists:
# On 2026-05-15, a worker code fix shipped to master but was never
# deployed — an operator had to manually `docker buildx build &&
# kubectl set image`. A user received the same broken expiry email
# twice as a result. This workflow eliminates that gap for every
# backend service, including this one.
#
# Build context note:
# The Dockerfile expects to be invoked from the parent of provisioner/,
# with sibling common/ and proto/ directories present. In CI we mirror
# that by checking out:
# . (workspace root)
# ├── provisioner/ (this repo)
# ├── common/ (sibling repo)
# └── proto/ (sibling repo)
# then `docker buildx build -f provisioner/Dockerfile .` from root.
#
# The Dockerfile has two named final stages (debug, prod). The default
# when no --target is passed is the LAST named stage — `prod` — exactly
# what we want here.
#
# Required repo secret:
# KUBECONFIG_B64 — base64-encoded kubeconfig with permission to
# `kubectl set image deployment/instant-provisioner -n instant-infra`.
#
# GHCR auth uses the per-job GITHUB_TOKEN with `packages: write`.
name: Deploy
on:
push:
branches: [master]
# CI-minute savings (2026-05-21): skip Deploy on docs-only commits.
paths-ignore:
- '**.md'
- 'docs/**'
- 'CLAUDE.md'
- '.gitignore'
- 'LICENSE'
- 'BUGBASH-*/**'
workflow_dispatch:
concurrency:
# CI-minute savings (2026-05-21): cancel prior in-flight Deploy when a
# newer commit lands on the same branch.
group: deploy-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
packages: write
env:
IMAGE_REPO: ghcr.io/instanode-dev/instant-provisioner
K8S_NAMESPACE: instant-infra
K8S_DEPLOYMENT: instant-provisioner
K8S_CONTAINER: provisioner
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout provisioner (this repo) into ./provisioner
uses: actions/checkout@v6
with:
path: provisioner
- name: Checkout common sibling into ./common
uses: actions/checkout@v6
with:
repository: ${{ vars.COMMON_REPO || format('{0}/common', github.repository_owner) }}
# 2026-05-15: GITHUB_TOKEN is scoped to THIS repo only and 404s
# on private sibling repos. REPO_ACCESS_TOKEN is a fine-grained
# PAT with read access to InstaNode-dev/{common,proto}.
token: ${{ secrets.REPO_ACCESS_TOKEN }}
path: common
- name: Checkout proto sibling into ./proto
uses: actions/checkout@v6
with:
repository: ${{ vars.PROTO_REPO || format('{0}/proto', github.repository_owner) }}
token: ${{ secrets.REPO_ACCESS_TOKEN }}
path: proto
- name: Compute build metadata
id: meta
run: |
SHORT_SHA="${GITHUB_SHA:0:7}"
BUILD_TIME="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
VERSION="master-${SHORT_SHA}"
echo "short_sha=${SHORT_SHA}" >> "$GITHUB_OUTPUT"
echo "build_time=${BUILD_TIME}" >> "$GITHUB_OUTPUT"
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "Built ${VERSION} (${BUILD_TIME})"
- name: Set up Go (for unit tests + go.mod replace directives)
uses: actions/setup-go@v6
with:
go-version: '1.24'
- name: Run unit tests (short, no integration deps)
# go.mod uses `replace instant.dev/common => ../common` and
# `replace instant.dev/proto => ../proto`. When `go test` runs
# inside ./provisioner, the relative paths resolve to ./common
# and ./proto in the workspace root — already correct.
working-directory: provisioner
run: go test ./... -short -count=1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Log in to GHCR
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push image
# Build context = workspace root so Dockerfile's
# `COPY proto/`, `COPY common/`, `COPY provisioner/` all resolve.
run: |
docker buildx build \
--platform linux/amd64 \
-f provisioner/Dockerfile \
--build-arg GIT_SHA="${{ steps.meta.outputs.short_sha }}" \
--build-arg BUILD_TIME="${{ steps.meta.outputs.build_time }}" \
--build-arg VERSION="${{ steps.meta.outputs.version }}" \
-t "${IMAGE_REPO}:${{ steps.meta.outputs.version }}" \
-t "${IMAGE_REPO}:latest" \
--push \
.
- name: Set up kubectl
uses: azure/setup-kubectl@v5
with:
version: 'latest'
- name: Configure kubeconfig from KUBECONFIG_B64 secret
env:
KUBECONFIG_B64: ${{ secrets.KUBECONFIG_B64 }}
run: |
if [ -z "${KUBECONFIG_B64}" ]; then
echo "::error::KUBECONFIG_B64 repo secret is not set. Add it under Settings → Secrets → Actions."
exit 1
fi
mkdir -p "$HOME/.kube"
echo "$KUBECONFIG_B64" | base64 -d > "$HOME/.kube/config"
chmod 600 "$HOME/.kube/config"
kubectl version --client=true
- name: Roll out new image
run: |
IMAGE="${IMAGE_REPO}:${{ steps.meta.outputs.version }}"
echo "Setting ${K8S_DEPLOYMENT}.${K8S_CONTAINER} to ${IMAGE}"
kubectl set image \
"deployment/${K8S_DEPLOYMENT}" \
"${K8S_CONTAINER}=${IMAGE}" \
-n "${K8S_NAMESPACE}"
kubectl rollout status \
"deployment/${K8S_DEPLOYMENT}" \
-n "${K8S_NAMESPACE}" \
--timeout=180s
- name: Verify rolled-out image tag matches built SHA (rule-14 gate)
# CLAUDE.md rule 14 build-SHA gate. The provisioner has no public
# ingress (gRPC on :50051 + a /healthz sidecar on :8092, both
# cluster-internal), so there is no external URL to curl.
#
# An earlier revision tried `kubectl port-forward` of :8092 from the
# runner — that proved flaky (apiserver-proxied port-forward against
# a distroless pod behind the task-#115 NetworkPolicies returns empty
# bodies intermittently) and was a false deploy failure even though
# the pod was healthy.
#
# The deterministic, network-free gate: the image tag IS the SHA
# (`master-<short_sha>`). Assert the live deployment references the
# exact tag we built; `kubectl rollout status` above already
# confirmed the new pod passed its readinessProbe — which is itself
# an httpGet of /healthz:8092, so a Ready pod IS a /healthz-verified
# pod. Tag match + Ready = SHA verified in prod.
run: |
ROLLED=$(kubectl get deployment "${K8S_DEPLOYMENT}" -n "${K8S_NAMESPACE}" \
-o jsonpath="{.spec.template.spec.containers[?(@.name=='${K8S_CONTAINER}')].image}")
EXPECTED="${IMAGE_REPO}:${{ steps.meta.outputs.version }}"
echo "Live image: ${ROLLED}"
echo "Expected: ${EXPECTED}"
if [ "${ROLLED}" != "${EXPECTED}" ]; then
echo "::error::Rolled image (${ROLLED}) != expected (${EXPECTED})"
exit 1
fi
echo "Confirmed ${K8S_DEPLOYMENT} is running ${EXPECTED} (SHA ${{ steps.meta.outputs.short_sha }})"