# Local CI/CD Pipeline — Design
## Overview
This design replaces the GitHub-dependent CI/CD pipeline (ARC + GHCR) with a fully local pipeline using Gitea as the Git forge, Woodpecker CI for pipeline execution, and the existing local Docker registry at `registry.celestium.life` for image storage. The existing ArgoCD and Kargo infrastructure is retained for GitOps deployment and staged promotion, with configuration updates to point at local sources instead of GitHub/GHCR.
The migration touches five areas:
1. **Gitea configuration** — Complete initial setup (admin user, OAuth2 app), create the `stonks-oracle` repository, and configure webhooks for Woodpecker CI. Gitea is already deployed in the `git-server` namespace but unconfigured.
2. **Woodpecker CI deployment** — Deploy server and agent via the `woodpecker/woodpecker` Helm chart in the `woodpecker` namespace. The server authenticates with Gitea via OAuth2. The agent uses the Kubernetes backend, executing each pipeline step as a standalone Pod.
3. **Pipeline file** — Create `.woodpecker.yml` translating the existing GitHub Actions workflow into Woodpecker's native format, targeting the local registry and adding a GitHub mirror step.
4. **ArgoCD/Kargo updates** — Update ArgoCD repo secret to point at Gitea, update ArgoCD Applications to source from Gitea, update Kargo Warehouse to watch the local registry.
5. **ARC teardown** — Remove ARC controller, runner scale set, RBAC, PV, and `arc-system` namespace.
### Key Design Decisions
1. **Woodpecker with Kubernetes backend (not Docker-in-Docker agent)** — The Woodpecker agent uses `WOODPECKER_BACKEND: kubernetes`, executing each pipeline step as a standalone Pod in the `woodpecker` namespace. A temporary PVC is created per pipeline run to transfer files between steps. This avoids DinD complexity for most steps. Image builds use the `woodpeckerci/plugin-docker-buildx` plugin with privileged mode for the build step only.
2. **Gitea API for initial setup** — Gitea's initial setup (admin user creation, OAuth2 app registration, repo creation) is automated via Gitea's REST API in `runmefirst.sh`. This avoids manual web UI interaction and makes the setup reproducible.
3. **Single Helm chart for Woodpecker** — The `woodpecker/woodpecker` chart contains both server and agent subcharts. One `helm install` deploys both components. The agent connects to the server via the in-cluster service `woodpecker-server:9000`.
4. **NFS PV for Woodpecker** — Woodpecker server data (SQLite database, build logs) persists on an NFS volume at `nfs://192.168.42.8:/volume1/Kubernetes/pipelines/woodpecker`, surviving cluster rebuilds. The ARC PV is removed since ARC is being torn down.
5. **GitHub as read-only mirror** — After all CI steps pass, a final pipeline step pushes to GitHub via SSH key stored as a Woodpecker secret. GitHub mirror failure does not block image promotion or deployment.
6. **ArgoCD sources from Gitea** — ArgoCD's repo secret is updated to point at the Gitea repository URL. All three Applications (beta, paper, live) source Helm charts from Gitea instead of GitHub.
7. **Helm chart image registry update** — The base `values.yaml` changes `image.registry` from `ghcr.io/celesrenata/stonks-oracle` to `registry.celestium.life/stonks-oracle`. The `ghcrAuth` section and `ghcr-credentials` imagePullSecret are removed since the local registry requires no authentication.
## Architecture
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ Gremlin Cluster (4x NixOS) │
│ │
│ ┌─────────────────┐ ┌──────────────────┐ ┌───────────────────────────┐ │
│ │ git-server ns │ │ woodpecker ns │ │ argocd ns │ │
│ │ (pre-existing) │ │ (NEW) │ │ (existing, updated) │ │
│ │ │ │ │ │ │ │
│ │ Gitea │ │ WP Server │ │ ArgoCD Server │ │
│ │ 10.1.1.x:30300 │ │ (StatefulSet) │ │ (stonks-argocd. │ │
│ │ :30022 (SSH) │ │ stonks-ci. │ │ celestium.life) │ │
│ │ │ │ celestium.life │ │ │ │
│ │ Local Registry │ │ │ │ Repo: Gitea (updated) │ │
│ │ registry. │ │ WP Agent │ │ │ │
│ │ celestium.life │ │ (Deployment) │ │ │ │
│ │ :30500 │ │ K8s backend │ │ │ │
│ └─────────────────┘ └──────────────────┘ └───────────────────────────┘ │
│ │
│ ┌─────────────────┐ ┌──────────────────┐ ┌───────────────────────────┐ │
│ │ kargo ns │ │ stonks-beta ns │ │ stonks-oracle ns │ │
│ │ (existing, │ │ │ │ (live/production) │ │
│ │ updated) │ │ ArgoCD App: │ │ │ │
│ │ │ │ stonks-beta │ │ ArgoCD App: stonks-live │ │
│ │ Warehouse: │ │ images from │ │ images from │ │
│ │ local registry │ │ local registry │ │ local registry │ │
│ │ (updated) │ │ │ │ │ │
│ └─────────────────┘ └──────────────────┘ └───────────────────────────┘ │
│ │
│ NFS: nfs://192.168.42.8:/volume1/Kubernetes/pipelines/{argocd,kargo,woodpecker}│
└─────────────────────────────────────────────────────────────────────────────┘
```
### Pipeline Flow
```mermaid
graph LR
A[Git Push to Gitea] --> B[Webhook → Woodpecker CI]
B --> C[Lint + Test
Python ruff + pytest
Frontend vitest]
C --> D[Build + Push
all images to
local registry]
D --> E[Integration Tests
run_pipeline.sh]
E -->|pass| F[GitHub Mirror
git push]
E -->|fail| X[❌ Pipeline Failed]
F --> G[Kargo Warehouse
detects new tag
in local registry]
G --> H[Beta Stage
auto-promote]
H --> I{Market Hours?}
I -->|outside| J[Paper Stage]
I -->|during| K[🚫 Blocked]
K -->|break-glass| J
J --> L{Market Hours?}
L -->|outside| M[Live Stage
manual approval]
L -->|during| N[🚫 Blocked]
N -->|break-glass| M
```
## Components and Interfaces
### 1. Gitea Configuration (`pipelines/gitea/`)
Gitea is already deployed in the `git-server` namespace but needs initial setup. The configuration is automated via shell scripts that call Gitea's REST API.
**Setup Steps (in `runmefirst.sh`):**
1. **Complete initial setup** — POST to `http://:3000/` with admin credentials to complete the install wizard, or use the Gitea API to create the admin user if the instance is already initialized.
2. **Create OAuth2 application** — POST to `/api/v1/user/applications/oauth2` to register Woodpecker CI with callback URL `https://stonks-ci.celestium.life/authorize`. Store the returned `client_id` and `client_secret` for Woodpecker's Helm values.
3. **Create repository** — POST to `/api/v1/user/repos` to create `stonks-oracle` repository.
4. **Add Gitea remote to local repo** — Configure the local Git clone on gremlin-1 with the Gitea remote and push the existing codebase.
**Gitea Service Access:**
- Web UI: `http://gitea-http.git-server.svc.cluster.local:3000` (cluster-internal) / `10.1.1.x:30300` (NodePort)
- SSH: `:30022` (NodePort)
- API: `http://gitea-http.git-server.svc.cluster.local:3000/api/v1/`
**Webhook Configuration:**
Woodpecker CI automatically registers webhooks when a repository is activated through the Woodpecker dashboard or API. The webhook URL points to the Woodpecker server's internal service endpoint.
### 2. Woodpecker CI Server and Agent (`pipelines/woodpecker/`)
**Namespace:** `woodpecker`
**Helm Chart:** `woodpecker/woodpecker` from the [woodpecker-ci/helm](https://github.com/woodpecker-ci/helm) repository. Contains two subcharts: `server` and `agent`.
**Server Configuration:**
- StatefulSet with 1 replica
- Persistent volume for SQLite database and build data at `/var/lib/woodpecker`
- NFS-backed PV at `nfs://192.168.42.8:/volume1/Kubernetes/pipelines/woodpecker`
- Traefik ingress at `stonks-ci.celestium.life` with TLS via `ca-issuer`
- Gitea OAuth2 authentication via `WOODPECKER_GITEA=true`, `WOODPECKER_GITEA_URL`, `WOODPECKER_GITEA_CLIENT`, `WOODPECKER_GITEA_SECRET`
- `WOODPECKER_HOST=https://stonks-ci.celestium.life`
- `WOODPECKER_ADMIN=admin` (matches Gitea admin username)
**Agent Configuration:**
- Deployment with 2 replicas
- Kubernetes backend (`WOODPECKER_BACKEND: kubernetes`)
- Pipeline steps execute as standalone Pods in the `woodpecker` namespace
- Temporary PVC created per pipeline run for file transfer between steps
- `WOODPECKER_BACKEND_K8S_STORAGE_CLASS: ""` (use default)
- `WOODPECKER_BACKEND_K8S_VOLUME_SIZE: 10G`
- ServiceAccount with RBAC for creating Pods, Services, PVCs in the `woodpecker` namespace
- Additional ClusterRoleBinding for integration test steps that need to create ephemeral namespaces
**Helm Values Structure (`pipelines/woodpecker/values.yaml`):**
```yaml
server:
enabled: true
env:
WOODPECKER_HOST: "https://stonks-ci.celestium.life"
WOODPECKER_GITEA: "true"
WOODPECKER_GITEA_URL: "http://gitea-http.git-server.svc.cluster.local:3000"
WOODPECKER_GITEA_CLIENT: ""
WOODPECKER_GITEA_SECRET: ""
WOODPECKER_ADMIN: "admin"
ingress:
enabled: true
ingressClassName: traefik
hosts:
- host: stonks-ci.celestium.life
paths:
- path: /
backend:
serviceName: woodpecker-server
servicePort: 80
tls:
- secretName: woodpecker-tls
hosts:
- stonks-ci.celestium.life
annotations:
cert-manager.io/cluster-issuer: ca-issuer
persistentVolume:
enabled: true
size: 5Gi
storageClass: ""
agent:
enabled: true
replicaCount: 2
env:
WOODPECKER_SERVER: "woodpecker-server:9000"
WOODPECKER_BACKEND: kubernetes
WOODPECKER_BACKEND_K8S_NAMESPACE: woodpecker
WOODPECKER_BACKEND_K8S_VOLUME_SIZE: 10G
WOODPECKER_BACKEND_K8S_STORAGE_RWX: "true"
```
**Network Policy:**
A NetworkPolicy in the `woodpecker` namespace allows Traefik ingress traffic to the Woodpecker server on its HTTP port (80).
### 3. Woodpecker Pipeline File (`.woodpecker.yml`)
The pipeline file translates the existing GitHub Actions workflow into Woodpecker's native format. Each step runs as a Docker container.
**Pipeline Structure:**
```
.woodpecker.yml
├── lint-python (ruff check services/)
├── test-python (pytest tests/)
├── test-frontend (npm ci && npx vitest --run)
├── build- (×12 Python services, sequential or grouped)
├── build-dashboard (frontend/Dockerfile)
├── build-superset (docker/Dockerfile.superset)
├── integration-test (run_pipeline.sh)
└── mirror-github (git push to GitHub)
```
**Key Differences from GitHub Actions:**
- No `uses:` syntax — each step specifies an `image:` and `commands:` or uses a Woodpecker plugin
- Image builds use `woodpeckerci/plugin-docker-buildx` plugin with `settings.repo`, `settings.registry`, `settings.tags`
- Branch filtering via `when: { branch: main, event: push }` instead of GitHub's `if:` conditions
- Secrets referenced via `from_secret:` instead of `${{ secrets.X }}`
- No matrix builds in Woodpecker — services are built sequentially or via multiple steps
**Image Tagging:**
All images pushed to `registry.celestium.life/stonks-oracle/:` and `registry.celestium.life/stonks-oracle/:latest`.
**GitHub Mirror Step:**
Uses the `woodpeckerci/plugin-git-push` plugin or a custom step with `git push --mirror` using an SSH deploy key stored as a Woodpecker secret.
### 4. ArgoCD Updates
**Repo Secret Update (`pipelines/argocd/repo-secret.yaml`):**
Change the repository URL from GitHub to Gitea:
```yaml
stringData:
url: http://gitea-http.git-server.svc.cluster.local:3000/admin/stonks-oracle.git
type: git
username: admin
password:
```
**Application Updates (`pipelines/argocd/apps/*.yaml`):**
All three Applications (stonks-beta, stonks-paper, stonks-live) update `spec.source.repoURL` from `https://github.com/celesrenata/stonks-oracle.git` to the Gitea repository URL.
### 5. Kargo Warehouse Update
**Warehouse Update (`pipelines/kargo/warehouse.yaml`):**
Change the image subscription from GHCR to the local registry:
```yaml
spec:
subscriptions:
- image:
repoURL: registry.celestium.life/stonks-oracle/query-api
```
Kargo stages, project, project-config, and market-hours AnalysisTemplate remain unchanged.
### 6. Helm Chart Updates (`infra/helm/stonks-oracle/`)
**`values.yaml` changes:**
```yaml
image:
registry: registry.celestium.life/stonks-oracle # was: ghcr.io/celesrenata/stonks-oracle
pullPolicy: Always
tag: latest
# REMOVED: imagePullSecrets, ghcrAuth sections
```
**`values-beta.yaml` and `values-paper.yaml`:**
No changes needed — they inherit `image.registry` from the base `values.yaml` and only override `image.tag`.
### 7. ARC Teardown
The `runmefirst.sh` script tears down ARC before installing Woodpecker:
1. `helm uninstall arc-runner-set --namespace arc-system || true`
2. `helm uninstall arc --namespace arc-system || true`
3. `kubectl delete -f arc/runner-rbac.yaml --ignore-not-found`
4. `kubectl delete pv pipeline-arc-pv --ignore-not-found`
5. `kubectl delete namespace arc-system --ignore-not-found`
The `pipelines/arc/` directory and `pipelines/pvs/arc-pv.yaml` are removed from the repo.
### 8. NFS Persistent Volumes
**Updated PV set** (ARC PV removed, Woodpecker PV added):
| PV Name | NFS Path | Capacity | Bound To |
|---|---|---|---|
| `pipeline-argocd-pv` | `/volume1/Kubernetes/pipelines/argocd` | 5Gi | PVC in `argocd` ns |
| `pipeline-kargo-pv` | `/volume1/Kubernetes/pipelines/kargo` | 2Gi | PVC in `kargo` ns |
| `pipeline-woodpecker-pv` | `/volume1/Kubernetes/pipelines/woodpecker` | 5Gi | PVC in `woodpecker` ns |
### 9. Updated `runmefirst.sh`
```
#!/bin/bash
set -euo pipefail
# 1. Tear down ARC (if present)
# - Uninstall ARC Helm releases
# - Delete RBAC, PV, namespace
# 2. Create namespaces (woodpecker, argocd, kargo, stonks-beta, stonks-paper)
# 3. Create NFS PVs (argocd, kargo, woodpecker)
# 4. Configure Gitea
# - Complete initial setup via API
# - Create admin user (if needed)
# - Create OAuth2 app for Woodpecker
# - Create stonks-oracle repository
# 5. Install Woodpecker CI via Helm
# - Inject Gitea OAuth2 client_id and client_secret into values
# - Apply NetworkPolicy for Traefik ingress
# 6. Install ArgoCD via Helm
# - Apply updated repo secret (pointing to Gitea)
# - Apply ArgoCD Applications
# 7. Install Kargo via Helm
# - Apply project, project-config, warehouse (local registry), stages
# 8. Apply Woodpecker agent RBAC for integration tests
```
### 10. Updated `runmelast.sh`
```
#!/bin/bash
set -euo pipefail
# Reverse order: Kargo → ArgoCD → Woodpecker
# Preserves: PVs, NFS data, git-server namespace (Gitea + registry)
# 1. Remove Kargo resources + Helm release
# 2. Remove ArgoCD resources + Helm release
# 3. Remove Woodpecker Helm release
# 4. Delete namespaces (woodpecker, argocd, kargo)
# 5. PVs intentionally NOT deleted
```
### 11. Woodpecker Agent RBAC
The Woodpecker agent's service account needs:
- **Namespace-scoped RBAC** (auto-created by Helm chart): Create/delete Pods, Services, PVCs in the `woodpecker` namespace for pipeline step execution.
- **ClusterRoleBinding** (manually applied): Grant the agent service account `cluster-admin` for integration test steps that create ephemeral namespaces and deploy sandbox infrastructure. This mirrors the existing ARC runner RBAC pattern.
```yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: woodpecker-agent-inttest
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: cluster-admin
subjects:
- kind: ServiceAccount
name: woodpecker-agent
namespace: woodpecker
```
## Data Models
### Pipeline Infrastructure Layout
```
~/sources/kube/pipelines/
├── runmefirst.sh # Full install: ARC teardown → Gitea config → Woodpecker → ArgoCD → Kargo
├── runmelast.sh # Teardown: Kargo → ArgoCD → Woodpecker (preserves PVs, git-server)
├── gitea/
│ └── setup.sh # Gitea API setup: admin user, OAuth2 app, repo creation
├── woodpecker/
│ ├── values.yaml # Woodpecker Helm values (server + agent)
│ └── network-policy.yaml # NetworkPolicy for Traefik → Woodpecker server
│ └── agent-rbac.yaml # ClusterRoleBinding for integration test access
├── argocd/
│ ├── values.yaml # ArgoCD Helm values (unchanged)
│ ├── repo-secret.yaml # Updated: points to Gitea instead of GitHub
│ └── apps/
│ ├── stonks-beta.yaml # Updated: repoURL → Gitea
│ ├── stonks-paper.yaml # Updated: repoURL → Gitea
│ └── stonks-live.yaml # Updated: repoURL → Gitea
├── kargo/
│ ├── values.yaml # Kargo Helm values (unchanged)
│ ├── project.yaml # Kargo Project (unchanged)
│ ├── project-config.yaml # Kargo ProjectConfig (unchanged)
│ ├── warehouse.yaml # Updated: watches local registry
│ ├── market-hours-check.yaml # AnalysisTemplate (unchanged)
│ └── stages/
│ ├── beta.yaml # Kargo Stage (unchanged)
│ ├── paper.yaml # Kargo Stage (unchanged)
│ └── live.yaml # Kargo Stage (unchanged)
└── pvs/
├── argocd-pv.yaml # NFS PV for ArgoCD (unchanged)
├── kargo-pv.yaml # NFS PV for Kargo (unchanged)
└── woodpecker-pv.yaml # NFS PV for Woodpecker (NEW, replaces arc-pv.yaml)
```
### Removed Files
```
pipelines/arc/ # Entire directory removed
├── values.yaml
├── runner-scaleset.yaml
└── runner-rbac.yaml
pipelines/pvs/arc-pv.yaml # ARC PV removed
```
### Image Tag Flow (Updated)
```
Git SHA (e.g., abc123)
→ Woodpecker builds: registry.celestium.life/stonks-oracle/:abc123
→ Integration test: run_pipeline.sh --image-tag abc123
→ GitHub mirror: git push (non-blocking)
→ Kargo Warehouse detects: abc123 in local registry
→ Kargo Freight created: abc123
→ Beta: helm upgrade with image.tag=abc123
→ Paper: helm upgrade with image.tag=abc123 (after market-hours check)
→ Live: helm upgrade with image.tag=abc123 (after approval + market-hours check)
```
### Kargo Resource Relationships (Updated)
```mermaid
graph TD
W[Warehouse: stonks-images
watches LOCAL REGISTRY
registry.celestium.life] -->|produces| F[Freight
image tag = git SHA]
F -->|auto-promote| SB[Stage: beta
ArgoCD App: stonks-beta]
SB -->|verified → available| SP[Stage: paper
market-hours verification
ArgoCD App: stonks-paper]
SP -->|verified → available| SL[Stage: live
manual approval + market-hours
ArgoCD App: stonks-live]
```
## Error Handling
### Gitea Setup Failures
| Failure | Detection | Recovery |
|---|---|---|
| Gitea not reachable | API call returns connection error | Check Gitea pod status in `git-server` namespace. Verify NodePort service. |
| Admin user already exists | API returns 422 | Script continues — idempotent. |
| OAuth2 app already exists | API returns 422 | Script queries existing apps and reuses credentials. |
| Repository already exists | API returns 409 | Script continues — idempotent. |
### Woodpecker Deployment Failures
| Failure | Detection | Recovery |
|---|---|---|
| Helm install fails | Non-zero exit | Check Helm chart repo access. Verify `woodpecker` namespace exists. |
| Server can't reach Gitea | OAuth2 login fails | Verify `WOODPECKER_GITEA_URL` resolves within cluster. Check Gitea service. |
| Agent can't connect to server | Agent logs show connection errors | Verify `WOODPECKER_SERVER` env var matches server service name. Check agent secret. |
| Pipeline step Pod fails to schedule | Pod stuck in Pending | Check node resources. Verify RBAC allows Pod creation in `woodpecker` namespace. |
| Image build fails (privileged) | Build step exits non-zero | Verify containerd/k3s allows privileged Pods. Check `plugin-docker-buildx` logs. |
### Pipeline Failures
| Failure | Detection | Recovery |
|---|---|---|
| Lint/test fails | Step exits non-zero | Fix code, push again. Build steps are skipped. |
| Image push to local registry fails | Plugin exits non-zero | Check registry health at `registry.celestium.life`. Verify DNS resolution. |
| Integration test fails | `run_pipeline.sh` exits non-zero | Check Woodpecker dashboard for step logs. Fix and re-push. |
| GitHub mirror fails | Mirror step exits non-zero | Non-blocking — images are already in local registry. Fix SSH key and re-run. |
### ArgoCD/Kargo Update Failures
| Failure | Detection | Recovery |
|---|---|---|
| ArgoCD can't clone from Gitea | Application shows "ComparisonError" | Verify repo secret credentials. Check Gitea accessibility from ArgoCD namespace. |
| Kargo can't reach local registry | Warehouse shows error | Verify `registry.celestium.life` DNS resolves. Check registry pod health. |
| Image pull fails (k3s nodes) | Pods stuck in ImagePullBackOff | Ensure k3s containerd trusts the local registry. Add registry mirror config if needed. |
### Rollback Strategy
Same as existing design:
- **Beta/Paper**: Promote a previous Freight in Kargo to roll back the image tag.
- **Live**: Same mechanism with manual approval required.
- **Emergency**: Direct `helm upgrade` with previous image tag.
## Testing Strategy
### Why Property-Based Testing Does Not Apply
This feature is entirely Infrastructure as Code: shell scripts, Kubernetes YAML manifests, Helm values files, and a Woodpecker pipeline YAML file. There are no pure functions, parsers, serializers, or business logic with meaningful input variation. PBT requires universal properties across a wide input space — this feature has fixed configuration values and Kubernetes resource states. Running 100 iterations of "does the Woodpecker ingress have TLS enabled" adds no value over running it once.
### Testing Approach
The testing strategy uses three tiers:
#### Tier 1: Smoke Tests (Configuration Validation)
Run locally or in CI without a live cluster.
| Test | What It Validates | How |
|---|---|---|
| Manifest syntax | All YAML files parse correctly | `kubectl apply --dry-run=client -f ` |
| Helm template rendering | Woodpecker values produce valid K8s resources | `helm template` with values file |
| Pipeline file syntax | `.woodpecker.yml` is valid | Woodpecker CLI lint or YAML parse |
| Namespace isolation | Pipeline namespaces distinct from `stonks-oracle` and `git-server` | Grep manifests for namespace fields |
| NFS path separation | PVs use distinct subdirectories | Inspect PV YAML |
| Image registry references | All manifests reference `registry.celestium.life` not `ghcr.io` | Grep all YAML for registry URLs |
| No GHCR auth remnants | `ghcrAuth` and `ghcr-credentials` removed from Helm chart | Grep values.yaml |
| ArgoCD repo URL | All Applications point to Gitea, not GitHub | Inspect Application YAML |
| Kargo warehouse URL | Warehouse watches local registry | Inspect warehouse YAML |
#### Tier 2: Integration Tests (Live Cluster Verification)
Run after `runmefirst.sh` on the Gremlin cluster.
| Test | What It Validates | How |
|---|---|---|
| Gitea accessible | Web UI responds | `curl http://10.1.1.x:30300` |
| Gitea repo exists | `stonks-oracle` repo created | Gitea API query |
| Woodpecker server running | Pods healthy in `woodpecker` namespace | `kubectl get pods -n woodpecker` |
| Woodpecker dashboard accessible | Web UI responds at `stonks-ci.celestium.life` | `curl -k https://stonks-ci.celestium.life` |
| Woodpecker OAuth2 works | Login redirects to Gitea | Browser test |
| ArgoCD accessible | Web UI responds at `stonks-argocd.celestium.life` | `curl -k https://stonks-argocd.celestium.life` |
| ArgoCD syncs from Gitea | Applications sync successfully | `argocd app get stonks-beta` |
| Kargo Warehouse | Discovers images from local registry | `kubectl get freight -n stonks-oracle` |
| Local registry accessible | Registry responds | `curl https://registry.celestium.life/v2/_catalog` |
| TLS certificates | Ingresses have valid certs from `ca-issuer` | `openssl s_client` or cert-manager status |
| PV binding | PVCs bound to NFS PVs | `kubectl get pvc -n woodpecker` |
| ARC removed | No ARC pods, no `arc-system` namespace | `kubectl get ns arc-system` returns NotFound |
| End-to-end pipeline | Push triggers build, images land in local registry | Push a commit, verify in Woodpecker dashboard |
| End-to-end promotion | Image flows beta → paper → live | Trigger promotion, verify deployments update |
| Teardown preservation | After `runmelast.sh`, PVs and NFS data intact | Run teardown, check PVs and NFS mount |
#### Tier 3: Market-Hours and Break-Glass Tests
Unchanged from existing design — these tests validate Kargo behavior which is not modified.
| Test | What It Validates | How |
|---|---|---|
| Market-hours block | Promotion blocked during 09:30–16:00 ET | Run AnalysisTemplate during market hours |
| Market-hours allow | Promotion allowed outside hours | Run AnalysisTemplate outside hours |
| Break-glass override | Manual approval bypasses block | Use Kargo manual approval during hours |
| Break-glass audit | Records operator, timestamp, justification | Query Kargo audit trail |