Skip to content

Continuously deploy main #48

@jeffmccune

Description

@jeffmccune

Continuously Deploy Main

Goal

On every push to main, build a container image and deploy it to the Kubernetes cluster.

Architecture

The workflow has two jobs:

  1. Build - Reuse the existing container.yaml workflow to build, push, and sign the container image to ghcr.io/holos-run/holos-console.
  2. Deploy - Use kubectl with Kustomize to patch the Deployment image tag and apply it to the cluster.

Phases

Phase 1: Wire container.yaml as a reusable workflow

Add workflow_call trigger to container.yaml so the CD workflow can call it and receive the image tag as an output. This avoids duplicating the build logic.

Add inputs and outputs for workflow_call matching the existing workflow_dispatch interface.

Phase 2: Create the Kustomize overlay

Create deploy/cd/kustomization.yaml:

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

resources:
  - deployment.yaml

images:
  - name: ghcr.io/holos-run/holos-console
    newTag: PLACEHOLDER

Create deploy/cd/deployment.yaml containing the base Deployment resource stripped of status and server-managed fields. The workflow will use kustomize edit set image to replace the tag at deploy time.

Phase 3: Create the CD workflow

Create .github/workflows/cd.yaml triggered on pushes to main.

Job 1: build

  • Call the existing container.yaml as a reusable workflow via uses: ./.github/workflows/container.yaml.
  • Receives the image tag as an output.

Job 2: deploy

  • Depends on build.
  • Checks out the repo, sets up kubectl, writes kubeconfig from secret, runs kustomize edit set image then kubectl apply -k ..
  • Verifies rollout completes.

Phase 4: Secrets the platform operator must provide

The deploy job needs kubectl access to the Kubernetes cluster over the public internet. One GitHub repository secret is required:

Secret Name Description
KUBECONFIG_DATA Base64-encoded kubeconfig file scoped to the holos-console namespace

Steps for the platform operator:

  1. Create a ServiceAccount for CD on the cluster:

    kubectl -n holos-console create serviceaccount github-cd
  2. Create an RBAC Role and RoleBinding scoped to the holos-console namespace:

    kubectl -n holos-console create role github-cd \
      --verb=get,list,watch,patch,update \
      --resource=deployments,replicasets,pods
    
    kubectl -n holos-console create rolebinding github-cd \
      --role=github-cd \
      --serviceaccount=holos-console:github-cd
  3. Create a long-lived token for the ServiceAccount:

    kubectl -n holos-console apply -f - <<'EOF'
    apiVersion: v1
    kind: Secret
    metadata:
      name: github-cd-token
      annotations:
        kubernetes.io/service-account.name: github-cd
    type: kubernetes.io/service-account-token
    EOF
  4. Build a kubeconfig using the token and cluster CA:

    TOKEN=$(kubectl -n holos-console get secret github-cd-token -o jsonpath='{.data.token}' | base64 -d)
    CA=$(kubectl -n holos-console get secret github-cd-token -o jsonpath='{.data.ca\.crt}')
    SERVER="https://<cluster-api-endpoint>:6443"
    
    cat > /tmp/cd-kubeconfig.yaml <<EOF
    apiVersion: v1
    kind: Config
    clusters:
    - cluster:
        certificate-authority-data: ${CA}
        server: ${SERVER}
      name: odin
    contexts:
    - context:
        cluster: odin
        namespace: holos-console
        user: github-cd
      name: github-cd@odin
    current-context: github-cd@odin
    users:
    - name: github-cd
      user:
        token: ${TOKEN}
    EOF
  5. Add the GitHub secret:

    gh secret set KUBECONFIG_DATA < <(base64 -w0 /tmp/cd-kubeconfig.yaml)
    rm /tmp/cd-kubeconfig.yaml

Phase 5: Final cleanup

Scan for dead code, stale docs, or outdated references introduced by the plan.

Workflow sketch

name: CD

on:
  push:
    branches: [main]

jobs:
  build:
    uses: ./.github/workflows/container.yaml
    with:
      git_ref: refs/heads/main
    permissions:
      contents: read
      packages: write
      attestations: write
      id-token: write

  deploy:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup kubectl
        uses: azure/setup-kubectl@v4

      - name: Configure kubeconfig
        run: echo "$KUBECONFIG_DATA" | base64 -d > "$RUNNER_TEMP/kubeconfig"
        env:
          KUBECONFIG_DATA: ${{ secrets.KUBECONFIG_DATA }}

      - name: Deploy
        env:
          KUBECONFIG: ${{ runner.temp }}/kubeconfig
        run: |
          cd deploy/cd
          kustomize edit set image "ghcr.io/holos-run/holos-console=${{ needs.build.outputs.tag }}"
          kubectl apply -k .

      - name: Verify rollout
        env:
          KUBECONFIG: ${{ runner.temp }}/kubeconfig
        run: kubectl -n holos-console rollout status deployment/holos-console --timeout=120s

Test plan

  • Push a commit to main and verify the workflow builds the image and deploys it.
  • Verify the deployed image tag matches the build output.
  • Verify the rollout completes successfully.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions