k8s-portal: build off-infra GHA -> ghcr + Keel; remove Woodpecker build (no-local-builds)
Some checks failed
ci/woodpecker/push/default Pipeline was canceled

The last in-cluster image build. GHA build-k8s-portal.yml builds
ghcr.io/viktorbarzin/k8s-portal:latest+sha (path-filtered on the Dockerfile
dir); Keel (force/poll/match-tag) rolls the deployment. Stack image repointed
to ghcr (ignore_changed); .woodpecker/k8s-portal.yml deleted.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Viktor Barzin 2026-06-13 15:21:35 +00:00
parent 9501da81a0
commit b906f61ac3
3 changed files with 50 additions and 53 deletions

36
.github/workflows/build-k8s-portal.yml vendored Normal file
View file

@ -0,0 +1,36 @@
name: Build k8s-portal
# ADR-0002 / no-local-builds: k8s-portal (infra-owned Go portal) builds off-infra
# on GHA → public ghcr; Keel polls ghcr:latest and rolls the deployment. Replaces
# the in-cluster .woodpecker/k8s-portal.yml build.
on:
push:
branches: [master]
paths:
- 'stacks/platform/modules/k8s-portal/files/**'
workflow_dispatch: {}
permissions:
contents: read
packages: write
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/build-push-action@v6
with:
context: stacks/platform/modules/k8s-portal/files
platforms: linux/amd64
provenance: false
push: true
tags: |
ghcr.io/viktorbarzin/k8s-portal:latest
ghcr.io/viktorbarzin/k8s-portal:${{ github.sha }}

View file

@ -1,49 +0,0 @@
when:
event: push
branch: master
path:
include:
- "stacks/platform/modules/k8s-portal/files/**"
clone:
git:
image: woodpeckerci/plugin-git
settings:
attempts: 5
backoff: 10s
steps:
- name: build-and-push
image: woodpeckerci/plugin-docker-buildx
settings:
username: "viktorbarzin"
password:
from_secret: dockerhub-pat
repo: viktorbarzin/k8s-portal
dockerfile: stacks/platform/modules/k8s-portal/files/Dockerfile
context: stacks/platform/modules/k8s-portal/files
platforms:
- linux/amd64
tag: ["${CI_PIPELINE_NUMBER}", "latest"]
cache_from: "viktorbarzin/k8s-portal:latest"
cache_to: "type=inline"
- name: deploy
image: bitnami/kubectl:latest
commands:
- "kubectl set image deployment/k8s-portal portal=viktorbarzin/k8s-portal:${CI_PIPELINE_NUMBER} -n k8s-portal"
- "kubectl rollout status deployment/k8s-portal -n k8s-portal --timeout=120s"
- "echo 'k8s-portal deployed successfully (build ${CI_PIPELINE_NUMBER})'"
- name: slack
image: curlimages/curl
commands:
- |
curl -s -X POST -H 'Content-type: application/json' \
--data "{\"text\":\"K8s Portal: build #${CI_PIPELINE_NUMBER} ${CI_PIPELINE_STATUS}\"}" \
"$SLACK_WEBHOOK" || true
environment:
SLACK_WEBHOOK:
from_secret: slack_webhook
when:
status: [success, failure]

View file

@ -9,7 +9,7 @@ resource "kubernetes_namespace" "k8s_portal" {
metadata {
name = "k8s-portal"
labels = {
tier = var.tier
tier = var.tier
"keel.sh/enrolled" = "true"
}
}
@ -40,6 +40,15 @@ resource "kubernetes_deployment" "k8s_portal" {
metadata {
name = "k8s-portal"
namespace = kubernetes_namespace.k8s_portal.metadata[0].name
# ADR-0002 / no-local-builds: image now GHA-built -> ghcr:latest
# (.github/workflows/build-k8s-portal.yml). Keel polls ghcr:latest and rolls
# this deployment (replaces the removed Woodpecker in-cluster build+deploy).
annotations = {
"keel.sh/policy" = "force"
"keel.sh/trigger" = "poll"
"keel.sh/pollSchedule" = "@every 5m"
"keel.sh/match-tag" = "true"
}
labels = {
app = "k8s-portal"
tier = var.tier
@ -68,7 +77,7 @@ resource "kubernetes_deployment" "k8s_portal" {
spec {
container {
name = "portal"
image = "viktorbarzin/k8s-portal:latest"
image = "ghcr.io/viktorbarzin/k8s-portal:latest"
port {
container_port = 3000
}
@ -121,7 +130,8 @@ resource "kubernetes_deployment" "k8s_portal" {
# DRIFT_WORKAROUND: CI pipeline owns image tag (kubectl set image from Woodpecker/GHA); Kyverno mutates dns_config for ndots. Reviewed 2026-04-18.
ignore_changes = [
spec[0].template[0].spec[0].dns_config, # KYVERNO_LIFECYCLE_V1
spec[0].template[0].spec[0].container[0].image, # CI updates image tag
spec[0].template[0].spec[0].container[0].image, # Keel manages ghcr:latest digest
metadata[0].annotations["keel.sh/update-time"], # KEEL_LIFECYCLE_V1 (Keel stamps on roll)
]
}
}
@ -172,5 +182,5 @@ module "ingress_setup_script" {
ingress_path = ["/setup/script", "/agent"]
tls_secret_name = var.tls_secret_name
# auth = "none": Setup script + agent endpoint must be curl-able without auth (no cookies preserved in automation).
auth = "none"
auth = "none"
}