# 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 |