Bỏ qua

Deployment Log — K3s Infrastructure on UTM VMs

Mục đích: Ghi lại toàn bộ quá trình thiết lập hạ tầng K3s trên 3 UTM VMs (Apple Silicon / arm64),
bao gồm tất cả lỗi phát sinh và cách khắc phục từng bước.
Dùng làm tài liệu tham chiếu khi cần rebuild hoặc debug môi trường.


1. Tổng quan kiến trúc

Thành phần Vị trí Mô tả
fb-k3s-master 192.168.64.10 K3s control-plane + Docker Compose host cho stateful services
fb-k3s-worker-1 192.168.64.11 K3s worker node
fb-k3s-worker-2 192.168.64.12 K3s worker node

Stateful services (Docker Compose trên master): PostgreSQL, MongoDB, Redis, Kafka/ZooKeeper, Keycloak, OmniRoute, ClickHouse, SigNoz, LangFuse, MinIO
Stateless services (K3s/Helm trên cluster): 6 backend NestJS microservices + chatbot service


2. Chuẩn bị VMs trên UTM

2.1 Cấu hình từng VM

Tạo 3 VMs Ubuntu Server 24.04 LTS (ARM64) trên UTM với cấu hình:

VM Hostname vCPU RAM Disk IP tĩnh
Master fb-k3s-master 4 8 GB 80 GB 192.168.64.10/24
Worker 1 fb-k3s-worker-1 2 4 GB 40 GB 192.168.64.11/24
Worker 2 fb-k3s-worker-2 2 4 GB 40 GB 192.168.64.12/24

Network adapter: Shared Network (NAT) — dùng dải 192.168.64.x của UTM
Lý do chọn .10/.11/.12: Tránh conflict với DHCP của UTM (thường cấp từ .2.9)

2.2 Cấu hình IP tĩnh (Netplan)

Sau khi cài Ubuntu, cấu hình IP tĩnh cho mỗi VM qua Netplan. Interface thường là enp0s1 trên UTM ARM64.

# /etc/netplan/00-installer-config.yaml  (ví dụ cho master)
network:
  version: 2
  ethernets:
    enp0s1:
      dhcp4: false
      addresses:
        - 192.168.64.10/24
      routes:
        - to: default
          via: 192.168.64.1
      nameservers:
        addresses: [8.8.8.8, 8.8.4.4]
sudo netplan apply

2.3 Cấu hình SSH và sudo passwordless

Từ Mac, copy SSH public key vào từng VM:

ssh-copy-id -i ~/.ssh/utm_k3s.pub ubuntu@192.168.64.10
ssh-copy-id -i ~/.ssh/utm_k3s.pub ubuntu@192.168.64.11
ssh-copy-id -i ~/.ssh/utm_k3s.pub ubuntu@192.168.64.12

Trên mỗi VM, cấu hình sudo không cần password (bắt buộc cho Ansible):

echo 'ubuntu ALL=(ALL) NOPASSWD:ALL' | sudo tee /etc/sudoers.d/ubuntu-nopasswd

Kiểm tra: ansible -i inventory/utm all -m shell -a "sudo whoami" → phải trả về root trên cả 3 hosts.

2.4 Tạo vault password file

echo "your-vault-password" > ~/.vault_pass
chmod 600 ~/.vault_pass

3. Ansible Vault — Secrets

Tất cả secrets được lưu trong secrets/vault.yml (encrypted bằng AES-256).

Các biến đã khởi tạo:

Biến Vault Mô tả Format yêu cầu
vault_db_admin_password PostgreSQL admin password string
vault_redis_password Redis main password string
vault_chatbot_redis_password Redis chatbot session store string
vault_chatbot_db_password PostgreSQL chatbot user string
vault_keycloak_admin_password Keycloak admin string
vault_keycloak_client_secret Keycloak client secret hex string
vault_clickhouse_password ClickHouse default user alphanumeric only (dùng trong URL)
vault_jwt_secret JWT signing key min 32 chars
vault_jwt_encryption_key JWT encryption key đúng 32 chars
vault_internal_service_secret X-Internal-Service header hex string
vault_minio_root_password MinIO admin password string
vault_langfuse_nextauth_secret NextAuth secret min 32 chars
vault_langfuse_salt LangFuse password salt string
vault_langfuse_encryption_key LangFuse encryption key đúng 64 hex chars (openssl rand -hex 32)
vault_langfuse_redis_auth LangFuse Redis password string
vault_langfuse_init_password LangFuse admin password string
vault_langfuse_public_key LangFuse project public key pk-lf-...
vault_langfuse_secret_key LangFuse project secret key sk-lf-...
vault_groq_api_key Groq API key từ console.groq.com
vault_pinecone_api_key Pinecone API key từ app.pinecone.io

Generate secrets:

openssl rand -hex 32          # 64-char hex (langfuse_encryption_key, jwt keys)
openssl rand -base64 32       # base64 random string (passwords)

4. Các lần chạy Playbook và lỗi gặp phải

Nguyên tắc: Mọi fix đều phải thực hiện trong source (playbooks/roles/docker-compose), KHÔNG sửa trực tiếp trên VM.

Lần 1 — Lỗi callback plugin và deprecated apt modules

Lệnh chạy:

ansible-playbook -i inventory/utm playbooks/site.yml --vault-password-file ~/.vault_pass

Lỗi 1: community.general.yaml callback plugin ERROR

ERROR! Invalid callback requested: community.general.yaml

Nguyên nhân: ansible.cfg dùng stdout_callback = yaml nhưng plugin community.general.yaml bị xóa khỏi phiên bản mới của community.general.

Fix — ansible.cfg:

# Trước:
stdout_callback    = yaml

# Sau:
stdout_callback    = default
result_format      = yaml

Lỗi 2: apt_key / apt_repository deprecation trong Docker role

[DEPRECATION WARNING]: Module 'apt_key' is deprecated

Nguyên nhân: Ansible khuyến cáo không dùng apt_key từ Ubuntu 22.04+. Cần dùng gpg --dearmor theo phương pháp mới.

Fix — roles/docker/tasks/main.yml: Thay bằng shell commands:

- name: Add Docker GPG key
  shell: |
    curl -fsSL https://download.docker.com/linux/ubuntu/gpg | \
    gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
  args:
    creates: /usr/share/keyrings/docker-archive-keyring.gpg

- name: Add Docker apt repository
  shell: |
    echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] \
    https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | \
    tee /etc/apt/sources.list.d/docker.list > /dev/null
  args:
    creates: /etc/apt/sources.list.d/docker.list

Lần 2 — Lỗi sudo password và br_netfilter

Lỗi 3: Missing sudo password

Task failed: Missing sudo password

Nguyên nhân: Các VM chưa được cấu hình passwordless sudo.

Fix: SSH vào từng VM và chạy:

echo 'ubuntu ALL=(ALL) NOPASSWD:ALL' | sudo tee /etc/sudoers.d/ubuntu-nopasswd

(Đây là bước setup VM, không phải fix trong playbook)

Lỗi 4: net.bridge.bridge-nf-call-iptables: No such file or directory

sysctl: setting key "net.bridge.bridge-nf-call-iptables": No such file or directory

Nguyên nhân: Module kernel br_netfilter chưa được load, nên /proc/sys/net/bridge/... không tồn tại.

Fix — roles/common/tasks/main.yml: Thêm các tasks trước sysctl:

- name: Load br_netfilter kernel module
  shell: modprobe br_netfilter
  changed_when: false

- name: Persist br_netfilter on boot
  lineinfile:
    path: /etc/modules-load.d/k3s.conf
    line: br_netfilter
    create: yes
    mode: "0644"

Lần 3 — Lỗi Ingress template và Helm timeout

Lỗi 5: Ingress rules unmarshal error

cannot unmarshal object into Go struct field IngressSpec.spec.rules of type []v1.IngressRule

Nguyên nhân: Template dùng {{- if .Values.ingress.host }} với dấu - trim whitespace, làm mất khoảng trắng cần thiết trong YAML list item, render thành -host: thay vì - host:.

Fix — helm/ingress/templates/ingress.yaml: Viết lại hoàn toàn template, đặt http: trước host: có điều kiện:

spec:
  ingressClassName: nginx
  rules:
    - http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: {{ .Values.apiGateway.service }}
                port:
                  number: {{ .Values.apiGateway.port }}
      {{- if .Values.ingress.host }}
      host: {{ .Values.ingress.host }}
      {{- end }}

Lỗi 6: Helm chatbot deployment timeout

Error: context deadline exceeded (client timeout exceeded while waiting for Helm operation to complete)

Nguyên nhân: Image sangtrandev00/chatbot-service:latest chưa được push lên Docker Hub → pod ImagePullBackOff → Helm --wait timeout sau 120 giây.

Fix — roles/helm_apps/tasks/main.yml: Xóa --wait --timeout 120s khỏi cả 2 Helm deploy commands để không block playbook.


Lần 4 — Lỗi containers sau khi Docker Compose start

Lỗi 7: Keycloak restarting — JDBC H2 format error

URL format error; jdbc:h2 expected

Nguyên nhân: command: start --optimized yêu cầu image đã được build sẵn với JDBC drivers (pre-compiled). Image chuẩn của Keycloak 24 không có postgres driver pre-built, dùng H2 mặc định với --optimized.

Fix — docker-compose/stateful-stack.yml:

keycloak:
  command: start   # bỏ --optimized
  environment:
    KC_HEALTH_ENABLED: "true"
    # ... các env KC_DB khác

Lỗi 8: OTel collector restarting — cluster 'cluster' not found

Requested cluster 'cluster' not found

Nguyên nhân: SigNoz OTel collector v0.102.5 cần ClickHouse có cluster tên cluster để tạo distributed tables (ReplicatedMergeTree). ClickHouse standalone chưa có cấu hình cluster.

Fix — docker-compose/config/clickhouse-cluster.xml (file mới):

Dùng Kafka ZooKeeper đang chạy sẵn (zookeeper:2181) thay vì cài thêm ZooKeeper riêng:

<clickhouse>
  <zookeeper>
    <node><host>zookeeper</host><port>2181</port></node>
  </zookeeper>
  <remote_servers replace="true">
    <cluster>...</cluster>   <!-- cho SigNoz OTel -->
    <default>...</default>   <!-- cho LangFuse v3 -->
  </remote_servers>
  <macros>
    <shard>01</shard>
    <replica>clickhouse</replica>
    <cluster>cluster</cluster>
  </macros>
</clickhouse>

Ghi chú: Kafka ZooKeeper và ClickHouse dùng namespace ZooKeeper khác nhau (/kafka/clickhouse), không conflict.

Lỗi 9: LangFuse restarting — CLICKHOUSE_MIGRATION_URL not configured

CLICKHOUSE_MIGRATION_URL is not configured

Fix — docker-compose/observability-stack.yml: Thêm env var với đúng scheme:

CLICKHOUSE_MIGRATION_URL: clickhouse://default:${CLICKHOUSE_PASSWORD}@clickhouse:9000

Quan trọng: Phải dùng scheme clickhouse:// (native protocol, port 9000), KHÔNG phải http:// (HTTP interface, port 8123). LangFuse dùng Go's database/sql driver với clickhouse-go.

Lỗi 10: SigNoz query-service restarting — unable to open database file

unable to open database file: no such file or directory

Fix — docker-compose/observability-stack.yml: Thêm volume và env path:

query-service:
  environment:
    SIGNOZ_LOCAL_DB_PATH: /root/.signoz/signoz.db
  volumes:
    - signoz_data:/root/.signoz

Lỗi 11: arm64 vs amd64 — K3s pods exec format error

x86_64-binfmt-P: QEMU internal SIGSEGV

Nguyên nhân: Images sangtrandev00/*:latest được build cho amd64, UTM VMs chạy arm64 (Apple Silicon). QEMU user-space emulation không đủ ổn định cho Node.js.

Fix — roles/common/tasks/main.yml: Cài QEMU packages (partial fix, không đủ cho production):

- name: Install required packages
  apt:
    name:
      - qemu-user-static
      - binfmt-support

Kết luận: K3s app pods cần multi-arch images build với docker buildx --platform linux/amd64,linux/arm64. Đây là việc của CI/CD pipeline.


Lần 5–6 — ClickHouse Keeper không start

Lỗi 12: ClickHouse Keeper config không load

Ban đầu thử dùng built-in ClickHouse Keeper (không cần ZooKeeper ngoài). Nhưng config trong config.d/cluster.xml không được load trước khi ClickHouse start.

Code: 225. DB::Exception: Can't create replicated table without ZooKeeper. (NO_ZOOKEEPER)

Fix (lúc đó): Chuyển sang dùng Kafka ZooKeeper (zookeeper:2181) thay vì built-in Keeper. Đơn giản hơn và ZooKeeper đã chạy sẵn trên cùng Docker network.

Cập nhật (Kafka KRaft migration): Kafka bỏ Zookeeper hoàn toàn (chuyển sang KRaft, apache/kafka:4.3.1) nên ClickHouse không còn zookeeper nào để dùng ké. Đã thử lại built-in ClickHouse Keeper — lần này chạy được, verify bằng CREATE DATABASE ... ON CLUSTER cluster thành công (test với clickhouse/clickhouse-server:24.8-alpine, cùng version đang dùng). Khác biệt so với lần thất bại trước: file keeper-embedded.xml (chứa <keeper_server>) và cluster.xml (chứa <zookeeper> trỏ localhost:9181) được mount cùng lúc vào config.d/ trước khi container start lần đầu — không có bước nào update config sau khi ClickHouse đã chạy. Nếu deploy lên VM thật vẫn gặp lại lỗi NO_ZOOKEEPER, khả năng cao là do thứ tự copy file/mount volume của Ansible task — kiểm tra roles/obs_stack/tasks/main.yml đảm bảo cả 2 file config được copy TRƯỚC bước Start observability stack.

Thêm Ansible task restart ClickHouse khi config thay đổi — roles/stateful_stack/tasks/main.yml:

- name: Copy ClickHouse cluster config to master
  copy: ...
  register: clickhouse_config_result

- name: Restart ClickHouse if cluster config changed
  shell: |
    cd /home/ubuntu/stacks/football-booking
    docker compose -f observability-stack.yml restart clickhouse
  when: clickhouse_config_result.changed

Lý do cần task này: Docker Compose state: present không tự restart container khi nội dung bind-mounted config file thay đổi (chỉ restart khi container definition thay đổi).


Lần 7 — LangFuse cluster name conflict

Lỗi 13: LangFuse — cluster 'default' not found

error: failed to open database: code: 701, message: Requested cluster 'default' not found
CREATE TABLE schema_migrations ON CLUSTER default (...)

Nguyên nhân: - SigNoz OTel cần cluster tên cluster - LangFuse v3 hardcode cluster tên default trong migrations - Config ban đầu chỉ có cluster cluster

Fix — docker-compose/config/clickhouse-cluster.xml: Định nghĩa cả hai cluster pointing đến cùng shard:

<remote_servers replace="true">
  <cluster>   <!-- SigNoz OTel -->
    <shard><replica>...</replica></shard>
  </cluster>
  <default>   <!-- LangFuse v3 -->
    <shard><replica>...</replica></shard>
  </default>
</remote_servers>

Lỗi 14: SigNoz query-service — ClickHouse authentication failed

Authentication failed: password is incorrect, or there is no user with such name

Nguyên nhân: ClickHouseUrl: tcp://clickhouse:9000/?database=signoz_traces không có credentials. ClickHouse yêu cầu password cho TCP connections.

Fix — docker-compose/observability-stack.yml:

ClickHouseUrl: "tcp://clickhouse:9000/?username=default&password=${CLICKHOUSE_PASSWORD}&database=signoz_traces"

Lần 8 — Secrets rotation: CHANGE_ME placeholders

Lỗi 15: LangFuse worker — ENCRYPTION_KEY too short

ZodError: ENCRYPTION_KEY must be 256 bits, 64 string characters in hex format

Nguyên nhân: vault_langfuse_encryption_key vẫn còn giá trị placeholder "CHANGE_ME_64_char_hex_string" (28 chars). LangFuse worker v3 dùng Zod v4 validation — phải đúng 64 hex chars.

Phát hiện thêm: Nhiều biến vault khác vẫn còn CHANGE_ME. Cần update toàn bộ.

Quy trình rotate secrets:

# 1. Generate secrets
openssl rand -hex 32           # langfuse_encryption_key (64 chars)
openssl rand -hex 16           # jwt_encryption_key (32 chars), keycloak_client_secret
openssl rand -base64 32        # passwords

# 2. Decrypt vault
ansible-vault decrypt secrets/vault.yml --vault-password-file ~/.vault_pass

# 3. Edit file và replace tất cả CHANGE_ME values

# 4. Re-encrypt
ansible-vault encrypt secrets/vault.yml --vault-password-file ~/.vault_pass

Lần 9 — PostgreSQL password rotation

Lỗi 16: Keycloak và LangFuse-web — PostgreSQL authentication failed

FATAL: password authentication failed for user "football_admin"

Nguyên nhân: Khi đổi vault_db_admin_password, .env được update với password mới. Docker Compose recreate PostgreSQL container với password mới trong env. Nhưng PostgreSQL chỉ apply POSTGRES_PASSWORD khi data directory chưa tồn tại — data directory đã có từ lần khởi tạo trước với password cũ.

Fix — roles/stateful_stack/tasks/main.yml: Thêm task sync password sau mỗi lần start PostgreSQL:

- name: Wait for PostgreSQL to be ready
  shell: docker exec postgres pg_isready -U football_admin
  retries: 10
  delay: 3
  register: pg_ready
  until: pg_ready.rc == 0
  changed_when: false

- name: Sync PostgreSQL admin password with vault (handles vault password rotation)
  shell: |
    docker exec postgres psql -U football_admin -d postgres -c \
      "ALTER USER football_admin PASSWORD '{{ vault_db_admin_password }}';"
  changed_when: false

Tại sao dùng docker exec thay vì PostgreSQL module? PostgreSQL trong Docker dùng trust auth cho local unix socket connections. docker exec kết nối qua unix socket không cần password cũ → idempotent.


Lần 10 — LangFuse-web OOM

Lỗi 17: LangFuse-web — JavaScript heap out of memory

FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory

Nguyên nhân: LangFuse v3 Next.js app cần nhiều memory hơn limit cũ (--max-old-space-size=512, container limit 768M). Heap sử dụng ~517MB trong khi limit là 512MB.

Fix — docker-compose/observability-stack.yml:

langfuse-web:
  environment:
    NODE_OPTIONS: "--max-old-space-size=1024"   # tăng từ 512
  deploy:
    resources:
      limits:
        memory: 1.5G   # tăng từ 768M

5. Trạng thái cuối — Tất cả services sau 10 lần chạy playbook

Docker Compose (master — 192.168.64.10)

Container Image Port Status
postgres postgres:16-alpine 5432 ✅ healthy
mongodb mongo:7 27017 ✅ running
redis redis:7-alpine 6379 ✅ healthy
zookeeper cp-zookeeper:7.6.0 internal ✅ running
kafka cp-kafka:7.6.0 9093 (external) ✅ healthy
keycloak keycloak:24.0.1 8080 ✅ running
omni-route omniroute:latest 20128 ✅ healthy
clickhouse clickhouse-server:24.8-alpine 8123, 9000 (localhost) ✅ healthy
otel-collector signoz-otel-collector:0.102.5 4317, 4318 ✅ running
signoz-query-service query-service:0.52.0 internal ✅ running
signoz-frontend frontend:0.52.0 3301 ✅ running
langfuse-web langfuse:3 3100 ✅ running
langfuse-worker langfuse-worker:3 internal ✅ running
langfuse-redis redis:7-alpine internal ✅ running
langfuse-minio chainguard/minio 9090-9091 (localhost) ✅ running
chatbot-redis redis:7-alpine 6380 ✅ healthy

K3s Cluster

Component Status
fb-k3s-master ✅ Ready (control-plane)
fb-k3s-worker-1 ✅ Ready
fb-k3s-worker-2 ✅ Ready
ingress-nginx-controller ✅ Running
Backend services (6 pods) ❌ QEMU SIGSEGV — cần arm64 images
chatbot-service ❌ ImagePullBackOff — image chưa push

6. Vấn đề còn lại và hướng xử lý

K3s App Pods — arm64/amd64 mismatch

Vấn đề: Images được build cho linux/amd64. UTM VMs chạy Ubuntu ARM64. QEMU user-space emulation (qemu-user-static) không đủ ổn định cho Node.js production workloads — crash với SIGSEGV.

Giải pháp: Build multi-arch images trong CI/CD:

docker buildx build \
  --platform linux/amd64,linux/arm64 \
  --push \
  -t sangtrandev00/api-gateway:latest .

Hoặc trên GitHub Actions:

- uses: docker/build-push-action@v5
  with:
    platforms: linux/amd64,linux/arm64
    push: true
    tags: sangtrandev00/api-gateway:latest

Groq và Pinecone API Keys

Còn CHANGE_ME trong vault. Cần điền trước khi chatbot hoạt động:

ansible-vault edit secrets/vault.yml --vault-password-file ~/.vault_pass
# Điền vault_groq_api_key và vault_pinecone_api_key

7. Lệnh thường dùng

Chạy toàn bộ playbook

ansible-playbook -i inventory/utm playbooks/site.yml --vault-password-file ~/.vault_pass

Kiểm tra nhanh tất cả containers

ssh -i ~/.ssh/utm_k3s ubuntu@192.168.64.10 "docker ps --format 'table {{.Names}}\t{{.Status}}' | sort"

Kiểm tra K3s pods

export KUBECONFIG=~/.kube/football-k3s.yaml
kubectl get pods -n football-booking
kubectl get nodes

Xem logs một container

ssh -i ~/.ssh/utm_k3s ubuntu@192.168.64.10 "docker logs <container_name> --tail=20"

Edit vault

ansible-vault edit secrets/vault.yml --vault-password-file ~/.vault_pass

Rotate secrets và re-apply

ansible-vault decrypt secrets/vault.yml --vault-password-file ~/.vault_pass
# Edit file
ansible-vault encrypt secrets/vault.yml --vault-password-file ~/.vault_pass
ansible-playbook -i inventory/utm playbooks/site.yml --vault-password-file ~/.vault_pass

Test Ansible connectivity

ansible -i inventory/utm all -m ping
ansible -i inventory/utm all -m shell -a "sudo whoami"

8. Các pattern quan trọng rút ra

Pattern 1: Docker Compose không tự restart khi bind-mount file thay đổi

Docker Compose chỉ recreate container khi container definition thay đổi (image, env vars, volumes list). Nếu chỉ nội dung file bind-mount thay đổi, phải restart thủ công hoặc dùng Ansible task.

- name: Copy config file
  copy: ...
  register: config_result

- name: Restart service if config changed
  shell: docker compose -f stack.yml restart <service>
  when: config_result.changed

Pattern 2: PostgreSQL password rotation

PostgreSQL chỉ apply POSTGRES_PASSWORD env var khi data directory trống. Để rotate password: 1. Update vault 2. Chạy playbook (update .env, restart container với env mới) 3. Ansible task ALTER USER sync password từ vault vào DB

Pattern 3: ClickHouse ZooKeeper sharing

Trong dev, Kafka ZooKeeper (zookeeper:2181) có thể được dùng cho ClickHouse. Cả hai dùng namespace ZooKeeper riêng (/kafka vs /clickhouse). Tiết kiệm tài nguyên, đơn giản hơn ClickHouse Keeper.

Pattern 4: LangFuse v3 cluster naming

LangFuse v3 hardcode ON CLUSTER default trong migrations. SigNoz cần cluster tên cluster. Định nghĩa cả hai trong ClickHouse:

<remote_servers replace="true">
  <cluster>...</cluster>
  <default>...</default>
</remote_servers>

Pattern 5: URL encoding cho ClickHouse connection strings

# ĐÚNG (native protocol):
clickhouse://default:password@clickhouse:9000

# SAI (HTTP scheme không được clickhouse-go hỗ trợ):
http://default:password@clickhouse:8123

# Nếu password có special chars, cần URL encode:
clickhouse://default:my%40password@clickhouse:9000
# → Tốt hơn: dùng alphanumeric-only passwords cho ClickHouse

Tài liệu này được tạo sau khi hoàn thành thiết lập infrastructure.
Ngày: 2026-06-20
Số lần chạy playbook: 10
Số lỗi gặp và fix: 17


CI/CD Fix Log — 2026-06-29

  • Removed _example-caller-*.yml from .github/workflows/ (moved to docs/workflow-examples/)
  • Fixed phantom GitHub Actions runs caused by ${{ github.sha }} expression in workflow_call input description
  • Set GITOPS_TOKEN secret in football-booking-chatbot-service-k3s repo — GitOps push step now succeeds