wiki-vault/Sources/Dev/pbsii-cicd-pipeline.md
Travis Herbranson 34a268d8dc migration: copy 62 notes from pbs-projects and homelab-projects
Two-layer structure: Sources (raw notes) + Wiki (compile output)
Four domains: Dev (40), Venture (3), Homelab (23), Reference (0)
Includes CLAUDE.md spec, index pages at all levels, compile log

Co-Authored-By: Lovebug <lovebug@herbylab.dev>
2026-05-08 17:21:13 -04:00

319 lines
9.4 KiB
Markdown

---
created: 2026-04-18
path: Sources/Dev
project: pbsii-cicd-pipeline
status: active
tags:
- pbs
- pbsii
- github-actions
- ansible
- cicd
- docker
- deployment
type: project-plan
updated: 2026-04-18
---
# PBSII CI/CD Pipeline (Revised)
## Goal
Automate deployment of PBSII layers using GitHub Actions with a self-hosted
runner on `ustest1`. Each layer deploys independently based on which files
changed. Branch strategy controls target environment: `main` → production,
`staging` → staging. Start with the dashboard layer, then extend to
collector and parser.
## Context
- PBSII is a private monorepo on GitHub with subdirectories: `collector/`,
`parser/`, `dashboard/`, `n8n/`
- Ansible project (`wp-i`) handles all server config and Docker
deployments, lives on `ustest1`
- Each Docker container has its own folder and `docker-compose.yml` on the
server (e.g., `/opt/docker/dashboard/`)
- Ansible docker role currently deploys all containers in a loop —
migrating to per-container tags
- A standalone pbs-hub deploy role exists as a reference pattern
- Each PBSII layer deploys differently: dashboard is Docker, collector is
systemd, parser is Python/venv
- n8n workflows are version-controlled JSON — no CI/CD needed
## Architecture
```
GitHub Push (to dashboard/ on main)
→ GitHub signals self-hosted runner on ustest1
→ ustest1 already has wp-i, SSH keys, Ansible — everything it needs
→ Runs Ansible playbook with dashboard tag
→ Ansible SSHs to Linode production, deploys dashboard
→ Google Chat notification on success/failure
GitHub Push (to dashboard/ on staging)
→ Same flow, but Ansible targets staging server
```
## Why Self-Hosted Runner
- `ustest1` (Proxmox homelab) already has `wp-i`, SSH keys to Linode, and
Ansible installed
- No secrets to copy into GitHub cloud runners
- No cloning the `wp-i` repo onto temporary VMs
- No rebuilding Ansible environment on every run
- Solves the staging drift problem — staging deployments go through the
same Ansible path as production
## Phase 1: Self-Hosted Runner Setup
### 1a. Install GitHub Actions runner on ustest1
In the PBSII GitHub repo → Settings → Actions → Runners → New self-hosted
runner:
- [ ] Follow GitHub's instructions to download and configure the runner
agent on `ustest1`
- [ ] Install as a systemd service so it starts on boot
- [ ] Verify runner shows as "Idle" in GitHub repo settings
### 1b. Runner user permissions
- [ ] The runner agent runs as a user on `ustest1` — that user needs SSH
access to Linode (production + staging)
- [ ] Ensure that user has access to `wp-i` on `ustest1`
- [ ] Ensure Ansible vault password is accessible (file or environment
variable)
### 1c. Label the runner
Add labels to the runner for targeting:
- [ ] `self-hosted` (default)
- [ ] `ustest1` (custom, for clarity)
## Phase 2: Ansible Dashboard Role
### 2a. Create the dashboard Ansible role in wp-i
Use the existing pbs-hub deploy role as a reference pattern. The role
should:
- [ ] Copy dashboard source files to server at `/opt/docker/dashboard/`
- [ ] Copy `docker-compose.yml` and `Dockerfile` to the server
- [ ] Run `docker compose up -d --build` in the dashboard folder
- [ ] Healthcheck to verify dashboard is responding
### 2b. Tag the role
- [ ] Ensure the role can be called with `--tags dashboard`
- [ ] Verify it only touches the dashboard container, nothing else
### 2c. Environment targeting
The playbook needs to accept a variable (e.g., `target_env`) that
determines whether it deploys to production or staging:
- [ ] `target_env: production` → production Linode IP, production compose
overrides
- [ ] `target_env: staging` → staging Linode IP, staging compose overrides
This keeps the same role for both environments — only the target changes.
## Phase 3: GitHub Actions Workflow (Dashboard)
### 3a. Production workflow
File: `.github/workflows/deploy-dashboard.yml`
```yaml
name: Deploy Dashboard
on:
push:
branches: [main]
paths:
- 'dashboard/**'
jobs:
deploy:
runs-on: [self-hosted, ustest1]
steps:
- name: Checkout PBSII
uses: actions/checkout@v4
- name: Run Ansible playbook
run: |
cd /path/to/wp-i
ansible-playbook playbook.yml \
--tags dashboard \
-e target_env=production
- name: Notify Google Chat (success)
if: success()
run: |
curl -X POST "$GOOGLE_CHAT_WEBHOOK" \
-H 'Content-Type: application/json' \
-d '{"text": "✅ Dashboard deployed to production (commit: ${{
github.sha }})" }'
- name: Notify Google Chat (failure)
if: failure()
run: |
curl -X POST "$GOOGLE_CHAT_WEBHOOK" \
-H 'Content-Type: application/json' \
-d '{"text": "❌ Dashboard deploy FAILED (commit: ${{ github.sha
}})" }'
env:
GOOGLE_CHAT_WEBHOOK: ${{ secrets.GOOGLE_CHAT_WEBHOOK }}
```
### 3b. Staging workflow
File: `.github/workflows/deploy-dashboard-staging.yml`
Same as above but:
- Trigger: `branches: [staging]`
- Ansible flag: `-e target_env=staging`
- Notification says "staging" instead of "production"
Alternatively, this could be one workflow file with a branch conditional —
decide during implementation.
### 3c. Test the workflow
- [ ] Push a small change to `dashboard/` on `staging` branch first
- [ ] Watch the Actions tab in GitHub
- [ ] Verify dashboard redeploys on staging server
- [ ] Verify Google Chat notification arrives
- [ ] Verify pushing to other directories does NOT trigger this workflow
- [ ] Once staging is solid, test the `main` branch → production flow
## Phase 4: Extend to Other Layers
Once the dashboard pipeline is proven, repeat the pattern:
### 4a. Collector pipeline
File: `.github/workflows/deploy-collector.yml`
Trigger: `paths: ['collector/**']`
Ansible role needs to:
- [ ] Cross-compile Go binary in CI step (or build on server)
- [ ] Copy binary to server
- [ ] Restart the systemd service (`systemctl restart pbs-collector`)
- [ ] Verify collector responds on its HTTP API
Note: Go cross-compiles easily — building on `ustest1` for Linux/amd64
target and shipping the binary to Linode avoids needing Go installed on
production.
### 4b. Parser pipeline
File: `.github/workflows/deploy-parser.yml`
Trigger: `paths: ['parser/**']`
Ansible role needs to:
- [ ] Copy parser source to server
- [ ] Ensure Python venv exists with correct deps (uv sync)
- [ ] No restart needed — n8n triggers it on schedule
## Phase 5: Google Chat Notifications
### 5a. Setup
- [ ] Add `GOOGLE_CHAT_WEBHOOK` to GitHub Secrets (can use same webhook as
existing n8n alerts, or create a dedicated one for deployments)
- [ ] Decide notification format — commit hash, branch, layer name,
success/failure
### 5b. Notification content
Success: `✅ [layer] deployed to [env] — commit abc1234`
Failure: `❌ [layer] deploy FAILED on [env] — commit abc1234 — check Actions
tab`
## Repo Structure After Implementation
```
pbsii/
├── .github/
│ └── workflows/
│ ├── deploy-dashboard.yml
│ ├── deploy-dashboard-staging.yml
│ ├── deploy-collector.yml
│ ├── deploy-collector-staging.yml
│ ├── deploy-parser.yml
│ └── deploy-parser-staging.yml
├── collector/
│ ├── README.md
│ ├── go.mod
│ ├── main.go
│ ├── Makefile
│ └── pbs-collector.service
├── parser/
│ ├── README.md
│ ├── pyproject.toml
│ └── parser.py
├── dashboard/
│ ├── README.md
│ ├── pyproject.toml
│ ├── Dockerfile
│ ├── docker-compose.yml
│ └── app.py
├── n8n/
│ └── workflows/
│ └── *.json
├── sql/
│ └── schema.sql
└── README.md
```
Ansible roles live in `wp-i`, not in this repo:
```
wp-i/
└── roles/
├── dashboard/
├── collector/
└── parser/
```
## GitHub Secrets Required
- [ ] `GOOGLE_CHAT_WEBHOOK` — webhook URL for deployment notifications
- [ ] `ANSIBLE_VAULT_PASSWORD` — if vault is needed for deploy (may not be
needed if runner user has access to vault password file on `ustest1`)
Note: SSH keys and Ansible inventory are already on `ustest1` — no need to
store them in GitHub.
## Open Questions
- [ ] What is the path to `wp-i` on `ustest1`? Needed for the workflow `cd`
step.
- [ ] Which user does the runner agent run as on `ustest1`? That user needs
SSH + Ansible access.
- [ ] Does `ustest1` currently have SSH keys to both production and staging
Linode? If not, set up during Phase 1.
- [ ] One workflow file per layer per environment (6 files), or one per
layer with branch conditionals (3 files)? Start simple, refactor if it gets
noisy.
- [ ] Google Chat: dedicated deployment webhook or reuse existing?
## Key Principles
- Each layer deploys independently — a change to the parser never touches
the dashboard
- Ansible stays the deployment executor — GitHub Actions is just the trigger
- Self-hosted runner on `ustest1` — no secrets in the cloud, no rebuilding
environments
- Branch strategy solves staging drift — `staging` branch → staging server,
`main` → production
- Build on server for v1 — move to CI builds later if needed
- Google Chat notifications on every deploy — success and failure
- Start with dashboard, prove the pattern, then extend
- Test on staging first, always
...sent from Jenny & Travis