Add Gitea Actions CI, ArgoCD Application, and Gitea Packages wiring

- .gitea/workflows/ci.yml: build → seo:lint → e2e → image build/push →
  bump k8s image tag and commit back (paths-ignore + [skip ci] guard)
- argocd/bcs-website-application.yaml: ArgoCD App (internal Gitea repoURL, k8s path)
- k8s: image → Gitea Packages path; kustomization images newTag stanza
- ADR-0007 (Gitea Actions + Packages), supersedes GHCR/GitHub bits of ADR-0001
- Reconcile gitops-deployment-strategy.md to Gitea

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Brad Rodgers 2026-07-16 06:29:01 -04:00
parent cd91c4b97c
commit 3dab3bc9fb
8 changed files with 193 additions and 23 deletions

85
.gitea/workflows/ci.yml Normal file
View File

@ -0,0 +1,85 @@
# BlueCap Strategies CI — Gitea Actions (ADR-0007)
#
# Pipeline: install → build (typecheck) → seo:lint → e2e → build & push image to Gitea Packages
# → bump the image tag in k8s/ and commit back to main. ArgoCD then reconciles k3s.
#
# PREREQUISITES (homelab, verify before the first run — see docs/gitops-deployment-strategy.md):
# 1. Docker must be usable from the job container. The runner mounts /var/run/docker.sock; if
# jobs can't see it, enable socket passthrough in act_runner config or switch this to kaniko.
# 2. The RUNNER HOST's dockerd must treat 10.66.15.22:3000 as an insecure (HTTP) registry
# ("insecure-registries": ["10.66.15.22:3000"] in /etc/docker/daemon.json).
# 3. K3S must trust the same HTTP registry — add a registries.yaml entry (k3s_registry_mirrors)
# for 10.66.15.22:3000, then restart k3s, or the pods can't pull the image.
# 4. The Gitea container package should be PUBLIC (simplest). If PRIVATE, add an imagePullSecret
# (see k8s/deployment.yaml) and grant the run token package:write.
# 5. secrets.GITEA_TOKEN (auto-injected per run) needs write access to this repo (tag-bump commit)
# and the package registry.
name: CI
on:
push:
branches: [main]
# Don't re-trigger on the CI tag-bump commit (touches k8s/) or docs-only changes.
paths-ignore:
- 'k8s/**'
- 'docs/**'
- 'planning/**'
- '**/*.md'
env:
IMAGE: 10.66.15.22:3000/bmr_bluecap/bcs-website
jobs:
ci:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Node 22
uses: actions/setup-node@v4
with:
node-version: 22
- name: Install dependencies
run: npm ci
- name: Build (typecheck + astro build)
run: npm run build
- name: SEO lint
run: npm run seo:lint
- name: End-to-end tests
run: |
npx playwright install --with-deps chromium
npm run test:e2e
- name: Compute image tag
id: tag
run: echo "sha=$(echo ${{ github.sha }} | cut -c1-12)" >> "$GITHUB_OUTPUT"
- name: Ensure docker + kustomize are available
run: |
command -v docker >/dev/null 2>&1 || { apt-get update && apt-get install -y docker.io; }
command -v kustomize >/dev/null 2>&1 || \
curl -sSL "https://github.com/kubernetes-sigs/kustomize/releases/download/kustomize%2Fv5.4.3/kustomize_v5.4.3_linux_amd64.tar.gz" \
| tar xz -C /usr/local/bin
- name: Build & push image to Gitea Packages
run: |
echo "${{ secrets.GITEA_TOKEN }}" | docker login 10.66.15.22:3000 -u "${{ github.actor }}" --password-stdin
docker build -t "$IMAGE:${{ steps.tag.outputs.sha }}" -t "$IMAGE:latest" .
docker push "$IMAGE:${{ steps.tag.outputs.sha }}"
docker push "$IMAGE:latest"
- name: Bump k8s image tag and commit back
run: |
git config user.name "gitea-actions[bot]"
git config user.email "gitea-actions@bmr_bluecap"
(cd k8s && kustomize edit set image "$IMAGE=$IMAGE:${{ steps.tag.outputs.sha }}")
git add k8s/kustomization.yaml
git diff --cached --quiet && { echo "no image change"; exit 0; }
git commit -m "ci: deploy ${{ steps.tag.outputs.sha }} [skip ci]"
git remote set-url origin "http://gitea-actions:${{ secrets.GITEA_TOKEN }}@10.66.15.22:3000/bmr_bluecap/bcs-website.git"
git push origin HEAD:main

View File

@ -0,0 +1,24 @@
# ArgoCD Application for the BlueCap Strategies site.
# Bootstrap: apply once (`kubectl apply -f argocd/bcs-website-application.yaml`) or add to the
# homelab-gitops app-of-apps. Kept OUT of k8s/ so ArgoCD does not try to manage this CR as part
# of the app itself. ArgoCD reaches Gitea over the internal HTTP URL (same as homelab-gitops).
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: bcs-website
namespace: argocd
spec:
project: default
source:
repoURL: http://10.66.15.22:3000/bmr_bluecap/bcs-website.git
targetRevision: main
path: k8s
destination:
server: https://kubernetes.default.svc
namespace: bluecap-strategies
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true

View File

@ -1,6 +1,8 @@
# ADR-0001: GitOps deployment via ArgoCD (not Flux) # ADR-0001: GitOps deployment via ArgoCD (not Flux)
**Status:** Accepted — 2026-07-15 **Status:** Accepted — 2026-07-15 · CI/registry specifics (GitHub Actions + GHCR) **superseded by
[ADR-0007](0007-gitea-actions-and-packages.md)** (Gitea Actions + Gitea Packages). The ArgoCD /
GitOps model here stands.
## Context ## Context

View File

@ -0,0 +1,33 @@
# ADR-0007: CI on Gitea Actions, images in Gitea Packages
**Status:** Accepted — 2026-07-15 · **Supersedes** the GitHub Actions + GHCR specifics of
[ADR-0001](0001-gitops-with-argocd.md) (the ArgoCD/GitOps model itself stands).
## Context
The repo lives on the homelab's self-hosted **Gitea** (`bmr_bluecap/bcs-website`), not GitHub, so
GitHub Actions and GHCR do not apply. Gitea runs an **`act_runner`** (GitHub-Actions-compatible)
and a built-in **container registry** (Gitea Packages, OCI `/v2/` confirmed live at
`10.66.15.22:3000`). The runner mounts `/var/run/docker.sock`; its label is `ubuntu-latest`
(jobs run in a `node:18-bullseye` container).
## Decision
- **CI = Gitea Actions**, workflow at `.gitea/workflows/ci.yml`, `runs-on: ubuntu-latest`.
- **Registry = Gitea Packages** at `10.66.15.22:3000/bmr_bluecap/bcs-website`, immutable
`<git-sha>` tags. Auth via the run's automatic token.
- Pipeline: `npm ci` → build (typecheck) → `seo:lint` → Playwright e2e → build & push image →
bump the image tag in `k8s/kustomization.yaml` and commit back to `main` (`[skip ci]` +
`paths-ignore` to avoid a loop). **ArgoCD** then reconciles k3s (unchanged from ADR-0001).
- **Package visibility: public** (recommended) — the image contains only the compiled public
site, so a public package removes the need for a cluster imagePullSecret. Private + an
External-Secrets-managed pull secret is the documented alternative.
## Consequences
- No external CI/registry dependency; everything stays in the homelab.
- **New infra prerequisites** (see `docs/gitops-deployment-strategy.md`): k3s must trust the HTTP
registry (`registries.yaml` / `k3s_registry_mirrors` entry for `10.66.15.22:3000`); the runner
host's Docker daemon needs `10.66.15.22:3000` as an insecure registry; docker must be usable
from job containers (act_runner socket passthrough) or the build switches to kaniko.
- `GHCR`/`GitHub` references in ADR-0001 and older doc revisions are superseded by this ADR.

View File

@ -14,3 +14,4 @@ Format: Status · Context · Decision · Consequences. Statuses: Proposed · Acc
| [0004](0004-reuse-monylog-for-monitoring.md) | Reuse the monylog stack for ops monitoring (not Uptime Kuma) | Accepted | | [0004](0004-reuse-monylog-for-monitoring.md) | Reuse the monylog stack for ops monitoring (not Uptime Kuma) | Accepted |
| [0005](0005-analytics-posthog-and-umami-trial.md) | Visitor analytics: PostHog + Umami side-by-side trial | Accepted | | [0005](0005-analytics-posthog-and-umami-trial.md) | Visitor analytics: PostHog + Umami side-by-side trial | Accepted |
| [0006](0006-launch-insights-blog.md) | Launch a blog at /insights/ | Accepted | | [0006](0006-launch-insights-blog.md) | Launch a blog at /insights/ | Accepted |
| [0007](0007-gitea-actions-and-packages.md) | CI on Gitea Actions, images in Gitea Packages | Accepted |

View File

@ -3,34 +3,49 @@
## Goal ## Goal
Deploy the BlueCap Strategies Astro site to the existing k3s cluster using GitOps, without Deploy the BlueCap Strategies Astro site to the existing k3s cluster using GitOps, without
giving GitHub Actions direct access to the homelab cluster. giving Gitea Actions direct access to the homelab cluster.
The homelab already runs **ArgoCD** in k3s, so this strategy uses ArgoCD as the reconciler The homelab already runs **ArgoCD** in k3s, so this strategy uses ArgoCD as the reconciler
rather than standing up a second GitOps controller (Flux). ArgoCD pulls from Git and GHCR; the rather than standing up a second GitOps controller (Flux). ArgoCD pulls from Git and the image
cluster is never exposed to GitHub. registry; the cluster is never exposed to the CI system.
Desired operating model: Desired operating model:
1. Edit the site locally in this repository. 1. Edit the site locally in this repository.
2. Commit changes. 2. Commit changes.
3. Push to `main`. 3. Push to `main`.
4. GitHub Actions runs tests and builds a container image. 4. Gitea Actions runs tests and builds a container image.
5. GitHub Actions pushes the image to GHCR, tagged with the immutable `<git-sha>`. 5. Gitea Actions pushes the image to Gitea Packages, tagged with the immutable `<git-sha>`.
6. Git is updated with the new desired image tag (CI commits the tag into `k8s/`). 6. Git is updated with the new desired image tag (CI commits the tag into `k8s/`).
7. ArgoCD, running inside k3s, detects the Git change. 7. ArgoCD, running inside k3s, detects the Git change.
8. ArgoCD syncs the Kubernetes manifests (Kustomize). 8. ArgoCD syncs the Kubernetes manifests (Kustomize).
9. k3s pulls the new image and rolls the website deployment. 9. k3s pulls the new image and rolls the website deployment.
10. Traefik serves the updated site publicly. 10. Traefik serves the updated site publicly.
GitHub never receives kubeconfig or direct network access to the homelab cluster. The cluster The CI runner never receives kubeconfig or direct network access to the homelab cluster. The cluster
pulls from Git and the image registry. pulls from Git and the image registry.
## Prerequisites (not yet done) > **Concrete implementation is Gitea, not GitHub/GHCR** — see [ADR-0007](adr/0007-gitea-actions-and-packages.md).
> CI = Gitea Actions (`.gitea/workflows/ci.yml`); registry = Gitea Packages
> (`10.66.15.22:3000/bmr_bluecap/bcs-website`); ArgoCD Application in `argocd/bcs-website-application.yaml`.
> Any lingering GitHub/GHCR wording below is the generic model; substitute Gitea/Gitea-Packages.
- **This repository is not yet a git repo.** GitOps requires a remote (GitHub) that both CI and ## Prerequisites
ArgoCD can read. `git init`, push to a GitHub repo, then wire ArgoCD to that repo.
- Decide the GHCR image path (replace the placeholder `ghcr.io/your-org/...` in Done: repo pushed to Gitea (`bmr_bluecap/bcs-website`, private); `k8s/deployment.yaml` image set to
`k8s/deployment.yaml`). the Gitea Packages path; ArgoCD Application manifest written.
**Still to do before the first deploy works (homelab infra):**
- **k3s must trust the HTTP registry.** Add a `registries.yaml` entry for `10.66.15.22:3000` as
insecure/HTTP (via the k3s role `k3s_registry_mirrors`), then restart k3s — otherwise pods can't
pull the image.
- **Runner host dockerd** needs `10.66.15.22:3000` in `insecure-registries` to push over HTTP.
- **Docker usable from job containers** (act_runner socket passthrough) — or switch the build to
kaniko.
- **Package visibility:** make the container package **public** (simplest; image is only the public
site), or keep it private and add an imagePullSecret (`k8s/deployment.yaml`), ideally via the
installed External Secrets operator.
- Apply the ArgoCD Application (once) or add it to the homelab-gitops app-of-apps.
## Cluster Findings ## Cluster Findings
@ -50,7 +65,7 @@ Edge / DNS / TLS (research 2026-06-05):
## Repository & Image Strategy ## Repository & Image Strategy
- **Mono-repo:** Keep site source and k8s manifests in the same repository. - **Mono-repo:** Keep site source and k8s manifests in the same repository.
- **GHCR:** Use GitHub Container Registry for images. - **Gitea Packages:** Use Gitea's built-in container registry (`10.66.15.22:3000`) for images.
- **Immutable tags:** Deploy via `<git-sha>` tags, never `latest`. `latest` defeats ArgoCD's - **Immutable tags:** Deploy via `<git-sha>` tags, never `latest`. `latest` defeats ArgoCD's
ability to detect and record what is actually running. ability to detect and record what is actually running.
- **Manifests:** Plain Kustomize under `k8s/` (already present). ArgoCD consumes - **Manifests:** Plain Kustomize under `k8s/` (already present). ArgoCD consumes
@ -76,7 +91,7 @@ metadata:
spec: spec:
project: default project: default
source: source:
repoURL: https://github.com/<owner>/bcs-website.git repoURL: http://10.66.15.22:3000/bmr_bluecap/bcs-website.git
targetRevision: main targetRevision: main
path: k8s path: k8s
destination: destination:
@ -99,7 +114,7 @@ spec:
Two options — this strategy recommends **Option A** for simplicity and auditability: Two options — this strategy recommends **Option A** for simplicity and auditability:
### Option A (recommended): CI commits the image tag ### Option A (recommended): CI commits the image tag
GitHub Actions, after pushing the image, patches the image tag in `k8s/` (via Gitea Actions, after pushing the image, patches the image tag in `k8s/` (via
`kustomize edit set image` or a `newTag` field) and commits back to `main`. ArgoCD auto-syncs the `kustomize edit set image` or a `newTag` field) and commits back to `main`. ArgoCD auto-syncs the
new tag. Every deploy is a Git commit traceable to a source SHA — clean rollback via `git revert`. new tag. Every deploy is a Git commit traceable to a source SHA — clean rollback via `git revert`.
@ -107,26 +122,26 @@ Add to `k8s/kustomization.yaml`:
```yaml ```yaml
images: images:
- name: ghcr.io/<owner>/bluecap-strategies-website - name: 10.66.15.22:3000/bmr_bluecap/bcs-website
newTag: <ci-sets-this> newTag: <ci-sets-this>
``` ```
### Option B: ArgoCD Image Updater ### Option B: ArgoCD Image Updater
Install ArgoCD Image Updater and annotate the Application to watch the GHCR repo and write back Install ArgoCD Image Updater and annotate the Application to watch the Gitea Packages repo and write back
the tag. Removes the CI commit-back step but adds a controller and registry-credential wiring. the tag. Removes the CI commit-back step but adds a controller and registry-credential wiring.
Only adopt if the homelab already runs Image Updater for other apps. Only adopt if the homelab already runs Image Updater for other apps.
## Phase 2: CI Pipeline (GitHub Actions) ## Phase 2: CI Pipeline (Gitea Actions)
Workflow on push to `main`: Workflow on push to `main`:
1. **Quality gate:** `npm ci`, `npm run typecheck`, `npm run build`. 1. **Quality gate:** `npm ci`, `npm run typecheck`, `npm run build`.
2. **E2E validation:** `npm run test:e2e` (Playwright) where browser deps are available. 2. **E2E validation:** `npm run test:e2e` (Playwright) where browser deps are available.
3. **Publish:** build the Docker image, push to GHCR tagged `<git-sha>`. 3. **Publish:** build the Docker image, push to Gitea Packages tagged `<git-sha>`.
4. **GitOps sync (Option A):** `kustomize edit set image ...=ghcr.io/<owner>/...:<git-sha>`, 4. **GitOps sync (Option A):** `kustomize edit set image ...=10.66.15.22:3000/bmr_bluecap/bcs-website:<git-sha>`,
commit and push to `main`. ArgoCD reconciles. commit and push to `main`. ArgoCD reconciles.
CI never touches the cluster — it only writes to GHCR and Git. CI never touches the cluster — it only writes to Gitea Packages and Git.
## Phase 3: Staging (Launch Readiness) ## Phase 3: Staging (Launch Readiness)
@ -139,7 +154,7 @@ Application tracking it.
### 1. Secret hygiene — Git-managed credentials ### 1. Secret hygiene — Git-managed credentials
Avoid manual `kubectl create secret`. Use **Sealed Secrets** or **External Secrets Operator** so Avoid manual `kubectl create secret`. Use **Sealed Secrets** or **External Secrets Operator** so
GHCR pull tokens and contact-form keys are encrypted in Git and the site is reproducible from Gitea Packages pull tokens and contact-form keys are encrypted in Git and the site is reproducible from
source alone. Match whichever the homelab ArgoCD stack already standardizes on. source alone. Match whichever the homelab ArgoCD stack already standardizes on.
### 2. Edge authority — Cloudflare hardening ### 2. Edge authority — Cloudflare hardening

View File

@ -13,9 +13,14 @@ spec:
labels: labels:
app: bluecap-website app: bluecap-website
spec: spec:
# If the Gitea package is PRIVATE, create a docker-registry secret (ideally via
# External Secrets) named `gitea-registry` in this namespace and uncomment:
# imagePullSecrets:
# - name: gitea-registry
containers: containers:
- name: website - name: website
image: ghcr.io/your-org/bluecap-strategies-website:latest # Tag is managed by CI via kustomize (see k8s/kustomization.yaml `images`).
image: 10.66.15.22:3000/bmr_bluecap/bcs-website:latest
imagePullPolicy: IfNotPresent imagePullPolicy: IfNotPresent
ports: ports:
- name: http - name: http

View File

@ -6,3 +6,8 @@ resources:
- deployment.yaml - deployment.yaml
- service.yaml - service.yaml
- ingress.yaml - ingress.yaml
# CI updates newTag to the immutable git-sha via `kustomize edit set image` (see .gitea/workflows/ci.yml).
images:
- name: 10.66.15.22:3000/bmr_bluecap/bcs-website
newTag: latest