diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..64eaad9 --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -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 diff --git a/argocd/bcs-website-application.yaml b/argocd/bcs-website-application.yaml new file mode 100644 index 0000000..6d5db7e --- /dev/null +++ b/argocd/bcs-website-application.yaml @@ -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 diff --git a/docs/adr/0001-gitops-with-argocd.md b/docs/adr/0001-gitops-with-argocd.md index 57bb723..4fefb6f 100644 --- a/docs/adr/0001-gitops-with-argocd.md +++ b/docs/adr/0001-gitops-with-argocd.md @@ -1,6 +1,8 @@ # 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 diff --git a/docs/adr/0007-gitea-actions-and-packages.md b/docs/adr/0007-gitea-actions-and-packages.md new file mode 100644 index 0000000..9378a11 --- /dev/null +++ b/docs/adr/0007-gitea-actions-and-packages.md @@ -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 + `` 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. diff --git a/docs/adr/README.md b/docs/adr/README.md index 07bf475..99bf139 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -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 | | [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 | +| [0007](0007-gitea-actions-and-packages.md) | CI on Gitea Actions, images in Gitea Packages | Accepted | diff --git a/docs/gitops-deployment-strategy.md b/docs/gitops-deployment-strategy.md index 4e417bc..34aa888 100644 --- a/docs/gitops-deployment-strategy.md +++ b/docs/gitops-deployment-strategy.md @@ -3,34 +3,49 @@ ## Goal 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 -rather than standing up a second GitOps controller (Flux). ArgoCD pulls from Git and GHCR; the -cluster is never exposed to GitHub. +rather than standing up a second GitOps controller (Flux). ArgoCD pulls from Git and the image +registry; the cluster is never exposed to the CI system. Desired operating model: 1. Edit the site locally in this repository. 2. Commit changes. 3. Push to `main`. -4. GitHub Actions runs tests and builds a container image. -5. GitHub Actions pushes the image to GHCR, tagged with the immutable ``. +4. Gitea Actions runs tests and builds a container image. +5. Gitea Actions pushes the image to Gitea Packages, tagged with the immutable ``. 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. 8. ArgoCD syncs the Kubernetes manifests (Kustomize). 9. k3s pulls the new image and rolls the website deployment. 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. -## 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 - 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 - `k8s/deployment.yaml`). +## Prerequisites + +Done: repo pushed to Gitea (`bmr_bluecap/bcs-website`, private); `k8s/deployment.yaml` image set to +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 @@ -50,7 +65,7 @@ Edge / DNS / TLS (research 2026-06-05): ## Repository & Image Strategy - **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 `` tags, never `latest`. `latest` defeats ArgoCD's ability to detect and record what is actually running. - **Manifests:** Plain Kustomize under `k8s/` (already present). ArgoCD consumes @@ -76,7 +91,7 @@ metadata: spec: project: default source: - repoURL: https://github.com//bcs-website.git + repoURL: http://10.66.15.22:3000/bmr_bluecap/bcs-website.git targetRevision: main path: k8s destination: @@ -99,7 +114,7 @@ spec: Two options — this strategy recommends **Option A** for simplicity and auditability: ### 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 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 images: - - name: ghcr.io//bluecap-strategies-website + - name: 10.66.15.22:3000/bmr_bluecap/bcs-website newTag: ``` ### 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. 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`: 1. **Quality gate:** `npm ci`, `npm run typecheck`, `npm run build`. 2. **E2E validation:** `npm run test:e2e` (Playwright) where browser deps are available. -3. **Publish:** build the Docker image, push to GHCR tagged ``. -4. **GitOps sync (Option A):** `kustomize edit set image ...=ghcr.io//...:`, +3. **Publish:** build the Docker image, push to Gitea Packages tagged ``. +4. **GitOps sync (Option A):** `kustomize edit set image ...=10.66.15.22:3000/bmr_bluecap/bcs-website:`, 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) @@ -139,7 +154,7 @@ Application tracking it. ### 1. Secret hygiene — Git-managed credentials 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. ### 2. Edge authority — Cloudflare hardening diff --git a/k8s/deployment.yaml b/k8s/deployment.yaml index 04e7225..4507454 100644 --- a/k8s/deployment.yaml +++ b/k8s/deployment.yaml @@ -13,9 +13,14 @@ spec: labels: app: bluecap-website 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: - 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 ports: - name: http diff --git a/k8s/kustomization.yaml b/k8s/kustomization.yaml index 11a22eb..59db4b1 100644 --- a/k8s/kustomization.yaml +++ b/k8s/kustomization.yaml @@ -6,3 +6,8 @@ resources: - deployment.yaml - service.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