# Cold Bore Ballistics — Website and Service Deployment Guide

**Version:** 1.0.0
**Date:** 2026-06-03
**Owner:** Cold Bore Ballistics LLC
**Audience:** Technically literate; no prior knowledge of CBB infrastructure assumed.

---

## Table of Contents

1. [What This Guide Covers](#1-what-this-guide-covers)
2. [Architecture Overview](#2-architecture-overview)
3. [Repositories](#3-repositories)
4. [Branch Model](#4-branch-model)
5. [The DEPLOY_SOURCE Mechanism](#5-the-deploy_source-mechanism)
6. [How Deployments Fire](#6-how-deployments-fire)
7. [Step-by-Step Operations](#7-step-by-step-operations)
8. [Anti-Patterns](#8-anti-patterns)
9. [Troubleshooting](#9-troubleshooting)

---

## 1. What This Guide Covers

Three repositories make up the CBB web presence. All three share the same deployment model, so this guide covers all of them.

| Repo | What It Is | Test Site | Production Site |
|---|---|---|---|
| `www.scope-dope.com` | ScopeDOPE product marketing site — static HTML | `https://www-test.scope-dope.com` | `https://www.scope-dope.com` |
| `www.coldboreballisticsllc.com` | Company site — static HTML | `https://www-test.coldboreballisticsllc.com` | `https://www.coldboreballisticsllc.com` |
| `feedback-service` | Feedback + `.dope` upload API — Python/Flask, PostgreSQL, Docker | `https://api-test.scope-dope.com` | `https://api.scope-dope.com` *(pending)* |

These repos are **independent of the ScopeDOPE mobile app** — they have their own versioning and release cycle. A change to the website has no relationship to the app version number.

---

## 2. Architecture Overview

### Test environment

Test sites run on a dedicated IP address (`172.31.1.200`) on the development workstation. nginx serves the static sites and proxies API traffic directly. The feedback service runs in Docker.

Deployments to the test environment are automated via GitHub Actions using a **self-hosted runner** — a lightweight process running on the dev workstation that connects outbound to GitHub, picks up queued jobs, and executes them locally. No inbound network access is required.

```
GitHub push → GitHub Actions triggers → self-hosted runner on dev workstation
  → git pull (static sites) / docker compose up (feedback-service)
  → nginx serves updated files immediately
```

### Production environment

Production deploys to Azure. The two marketing sites are hosted on **Azure Static Web Apps**; the feedback API is **Azure Container Apps** (deployment of the reworked feedback-service is tracked separately — Jira CBB-108). Deployments are triggered by `v*` version tags and run on **GitHub-hosted** runners.

```
git tag v1.2.3 → GitHub Actions (deploy-prod.yml, GitHub-hosted runner)
  → Azure/static-web-apps-deploy uploads repo content to the SWA
```

**Status: LIVE (2026-06-07).** Both static sites are deployed and serving from Azure:

| Site | Azure Static Web App | Custom domains |
|---|---|---|
| ScopeDOPE marketing | `swa-scopedope-prod` | `scope-dope.com`, `www.scope-dope.com` |
| CBB company | `swa-cbb-prod` | `coldboreballisticsllc.com`, `www.coldboreballisticsllc.com` |

Each repo has a `.github/workflows/deploy-prod.yml` that fires on a `v*` tag and uploads the repo-root static content to its SWA using the official `Azure/static-web-apps-deploy` action. The SWA deployment token is the repo secret `AZURE_SWA_DEPLOYMENT_TOKEN`. The SWAs themselves (and their custom-domain bindings) are provisioned by Terraform in `cbb-infra` (`services/scope-dope`, `services/cbb`); see `Azure_Architecture_Reference.md`.

---

## 3. Repositories

All repos follow the same conventions. Clone from the ColdBoreBallistics GitHub org:

```
git clone git@github.com:ColdBoreBallistics/www.scope-dope.com.git
git clone git@github.com:ColdBoreBallistics/www.coldboreballisticsllc.com.git
git clone git@github.com:ColdBoreBallistics/feedback-service.git
```

Each repo has two special branches beyond the usual `main` and feature branches:

| Branch | Purpose |
|---|---|
| `main` | Production-ready at all times. Pushes here may deploy to the test site. |
| `active-test` | Control branch. Contains `DEPLOY_SOURCE`. Do not delete. |

---

## 4. Branch Model

```
main          ← always deployable; triggers test deploy when DEPLOY_SOURCE == 'main'
├── feature/  ← short-lived work branches; merge to main when complete
└── (tags)    v1.0.0, v1.1.0 ...  ← trigger production deploys

active-test   ← control branch; contains DEPLOY_SOURCE file; never deleted
```

**`main`** is always in a deployable state. If a change isn't ready to be seen on the test site — or potentially deployed to production — it doesn't belong on `main` yet. Use a feature branch.

**`active-test`** is not a development branch. You never do work on it. Its sole purpose is to hold the `DEPLOY_SOURCE` file, which controls what the test sites serve. More on this below.

**Feature branches** are cut from `main`, named descriptively (e.g., `feature/new-homepage`, `fix/broken-nav-link`), and merged back to `main` when the work is ready to be tested or shipped. Keep them short-lived — days, not weeks.

**Tags** follow semantic versioning: `v<major>.<minor>.<patch>` (e.g., `v1.0.0`, `v1.2.3`). A tag signals "this commit is a production release." For the two static sites, pushing a `v*` tag deploys that commit to the site's Azure Static Web App via `deploy-prod.yml` (live as of 2026-06-07).

---

## 5. The DEPLOY_SOURCE Mechanism

### The problem it solves

The test site needs to be able to show any branch — `main`, a feature branch under development, a hotfix — without permanently redirecting the pipeline. At the same time, a push to `main` should normally auto-update the test site, so quick corrections don't require manual intervention.

A single workflow trigger (`push to main`) can't do both: it can't conditionally deploy from a different branch, and it can't be suppressed when you want the test site pointing elsewhere.

### How it works

The `active-test` branch contains a single plain-text file named `DEPLOY_SOURCE`. Its contents are exactly one branch name:

```
main
```

The deployment workflow reads this file every time it runs, then deploys from the branch it names — regardless of what triggered the workflow.

Two things trigger the workflow:
1. A push to `main`
2. A push to `active-test`

Combining these gives you full control:

| You push to... | DEPLOY_SOURCE contains | What happens |
|---|---|---|
| `main` | `main` | Test site updates from `main` ✓ |
| `main` | `feature/new-checkout` | Test site ignores the push; stays on `feature/new-checkout` |
| `active-test` | anything | Test site deploys from whatever the file says |

### Switching what the test site shows

To point the test site at a feature branch:

```bash
git checkout active-test
git pull origin active-test
echo "feature/new-checkout" > DEPLOY_SOURCE
git add DEPLOY_SOURCE
git commit -m "chore: point test site at feature/new-checkout"
git push origin active-test
```

The workflow fires immediately on that push. Within a minute or two the test site serves from `feature/new-checkout`.

To restore tracking of `main`:

```bash
git checkout active-test
git pull origin active-test
echo "main" > DEPLOY_SOURCE
git add DEPLOY_SOURCE
git commit -m "chore: restore test site to main"
git push origin active-test
```

---

## 6. How Deployments Fire

The workflow file lives at `.github/workflows/deploy-test.yml` in each repo. Here is what it does, in plain terms:

1. **Fetch the remote** — ensures the runner has the latest view of all branches, including `active-test`
2. **Read `DEPLOY_SOURCE`** — gets the branch name from the `active-test` branch on the remote
3. **Check whether to proceed** — if the trigger was a `main` push but `DEPLOY_SOURCE` doesn't say `main`, stop here; do nothing
4. **Check out the source branch** — switches the local repo to the branch named in `DEPLOY_SOURCE`
5. **Pull** — brings the local copy up to date
6. **Restart services** (feedback-service only) — rebuilds and restarts the Docker containers

For the static sites, nginx reads files from the local repo directory on every request. There is no "copy to webroot" step — the checkout IS the deployment.

### Production (`deploy-prod.yml`)

A separate workflow, `.github/workflows/deploy-prod.yml`, handles production. Unlike the
test workflow, it:

- runs on a **GitHub-hosted** runner (not the self-hosted box) — it does not depend on the
  dev workstation being online;
- triggers on a **`v*` tag** (not branch pushes);
- uploads the repo-root content to the site's **Azure Static Web App** with the official
  `Azure/static-web-apps-deploy` action, authenticated by the repo secret
  `AZURE_SWA_DEPLOYMENT_TOKEN`.

The two are independent: the test pipeline serves `www-test.*` from nginx; the production
pipeline serves the public domains from Azure SWA. A typical change flows test → validate →
tag → production.

---

## 7. Step-by-Step Operations

### 7.1 Making a change and watching it hit the test site

This is the everyday flow. `DEPLOY_SOURCE` is `main`.

```bash
# 1. Make your changes on main (or a feature branch you then merge)
git checkout main
# ... edit files ...
git add .
git commit -m "feat(site): update hero copy"
git push origin main
```

Go to **GitHub → repo → Actions**. You'll see the `Deploy — test` workflow running. When it completes (typically under a minute), the test site reflects your change.

### 7.2 Pointing the test site at a feature branch

Use this when you want to preview unreleased work on the test site without merging it to `main`.

```bash
# 1. Create and push your feature branch
git checkout -b feature/new-pricing-section
# ... do the work ...
git push origin feature/new-pricing-section

# 2. Point active-test at it
git checkout active-test
git pull origin active-test
echo "feature/new-pricing-section" > DEPLOY_SOURCE
git add DEPLOY_SOURCE
git commit -m "chore: point test site at feature/new-pricing-section"
git push origin active-test
# → workflow fires; test site now serves feature/new-pricing-section

# 3. Iterate freely — push changes to the feature branch
git checkout feature/new-pricing-section
# ... make changes ...
git push origin feature/new-pricing-section
# → active-test still points here; push to active-test again to re-deploy
git checkout active-test
git pull origin active-test
git commit --allow-empty -m "chore: redeploy test from feature/new-pricing-section"
git push origin active-test
```

### 7.3 Restoring the test site to track `main`

After feature branch testing is done:

```bash
# 1. Merge the feature branch to main (standard flow)
git checkout main
git merge --squash feature/new-pricing-section
git commit -m "feat(site): add pricing section"
git push origin main
# → test site does NOT update yet (DEPLOY_SOURCE still says feature/...)

# 2. Restore DEPLOY_SOURCE to main
git checkout active-test
git pull origin active-test
echo "main" > DEPLOY_SOURCE
git add DEPLOY_SOURCE
git commit -m "chore: restore test site to main"
git push origin active-test
# → test site now serves main; future main pushes auto-update it
```

### 7.4 Releasing to production (tagging)

When the test site is validated and the release is ready:

```bash
# Confirm you are on main and it is clean
git checkout main
git pull origin main
git status  # should be clean — committed content is what deploys

# Create and push the tag — this fires the production deploy to Azure
git tag v1.2.0
git push origin v1.2.0
```

Watch the deploy run (~1 minute):

```bash
gh run list -R ColdBoreBallistics/www.scope-dope.com --workflow=deploy-prod.yml --limit 1
# (substitute www.coldboreballisticsllc.com for the company site)
```

The `deploy-prod.yml` workflow runs on a **GitHub-hosted** runner and uploads the repo-root
content to the site's Azure Static Web App via `Azure/static-web-apps-deploy`. There is no
build step — the static files are uploaded as-is. When the run shows `success`, the live
site is updated (Azure SWA propagates to its edge within seconds).

**Do not tag from a feature branch.** Tags must always be on `main`.

### 7.5 Making an emergency fix to the live site

The live site is currently served from `main`. A fast fix:

```bash
# 1. Fix on main directly (or via a fast feature branch)
git checkout main
# ... fix the issue ...
git add .
git commit -m "fix(site): correct broken nav link"
git push origin main
# → if DEPLOY_SOURCE == 'main', test site updates immediately
# → verify on test site, then tag for production
git tag v1.0.1
git push origin v1.0.1
```

If `DEPLOY_SOURCE` is currently pointing at a feature branch (i.e., the test site isn't on `main`), restore it first (§7.3), verify the fix on test, then tag.

---

## 8. Anti-Patterns

**Committing work-in-progress to `main`.**
`main` is always deployable. A half-finished feature on `main` may be deployed to the test site automatically and could be tagged for production accidentally. Use a feature branch.

**Tagging without testing.**
The test site exists to catch problems before they reach production users. A tag skipped past the test environment is a production incident waiting to happen.

**Deleting `active-test`.**
`active-test` is a control branch. Deleting it breaks the deployment pipeline for everyone. If you accidentally delete it locally, the remote still has it — `git checkout -b active-test origin/active-test` recovers it. If it's deleted from the remote, recreate it from `main` and add `DEPLOY_SOURCE` back with `echo "main" > DEPLOY_SOURCE`.

**Force-pushing `active-test`.**
Don't. It rewrites history on a branch the pipeline depends on. If you made a mistake, push a correction commit instead.

**Long-lived feature branches.**
The longer a branch lives without merging, the harder the eventual merge becomes. Aim to merge feature branches within a few days. If a feature takes longer, keep it behind a flag or break it into smaller pieces.

**Tying version tags to the ScopeDOPE app version.**
Website versions are independent. `v1.0.0` on the website has no relationship to `v1.0.0` of the app. Version the website based on its own changes.

---

## 9. Troubleshooting

### Workflow didn't fire after a push

- Check **GitHub → repo → Actions** — it may still be queued or running
- Confirm the push went to `main` or `active-test` (not a feature branch — those don't trigger the test deploy)
- Check the self-hosted runner is online: on `mac001`, run `cd ~/actions-runner && ./svc.sh status` (or `gh api /orgs/ColdBoreBallistics/actions/runners --jq '.runners[] | {name, status}'`)

### Workflow failed at "Read DEPLOY_SOURCE"

- `active-test` may have been deleted, or `DEPLOY_SOURCE` is missing or empty
- Check: `git ls-remote origin active-test` — if no output, the branch is gone; recreate it (see §8 above)
- Check: `git show origin/active-test:DEPLOY_SOURCE` — if error, the file is missing; add it

### Test site didn't update after workflow completed successfully

- Verify the workflow deployed from the branch you expected (check the workflow run log — it prints "Deploy source: <branch>")
- For static sites: nginx reads directly from the repo; no further action needed. Check that the file changes are actually in the branch that was deployed
- `feedback-service` is currently **DEFERRED** (not deployed) — the `api-test.scope-dope.com` vhost and the `/api/` proxy on `www-test.scope-dope.com` return 502 until it is reactivated. That 502 is expected, not a deploy failure.

### Test site shows the wrong branch

- Read the current `DEPLOY_SOURCE`: `git show origin/active-test:DEPLOY_SOURCE`
- If it's not what you expect, update it per §5 and push to `active-test`

### Production site not updating after tag

The two static sites deploy to Azure SWA on a `v*` tag via `deploy-prod.yml`. If the live
site doesn't update:

1. **Check the workflow run:** `gh run list -R ColdBoreBallistics/<repo> --workflow=deploy-prod.yml --limit 1`. If it's `failure`, open the run to see why.
2. **Token expired/rotated?** The deploy fails if `AZURE_SWA_DEPLOYMENT_TOKEN` is stale. Re-fetch and reset it (see the repo's `DEPLOYMENT.md` → "Deployment token").
3. **Tagged the wrong commit?** The deploy uploads the commit the tag points at — confirm your content was committed to `main` *before* tagging.
4. **Edge cache:** Azure SWA propagates within seconds; a hard refresh (or checking from a different network) rules out local browser caching.

> The **feedback-service** production deploy to Azure Container Apps is a separate, still-pending piece (the service rework is tracked in Jira CBB-108). The two static sites are live.

---

## Document Control

| Field | Value |
|---|---|
| **Document owner** | `jschwefel@coldboreballisticsllc.com` |
| **Classification** | Confidential (03_-_Confidential/) |
| **Location** | `OneDrive/CBB_Corporate/03_-_Confidential/Infrastructure/Websites/Website_and_Service_Deployment_Guide.md` |
| **Cross-references** | `Azure_Architecture_Reference.md`; `Internal_Site_Operations.md` |
| **Created** | 2026-06-06 |
| **Last updated** | 2026-06-06 |
| **Next full review due** | 2027-06-06 |

---

## Revision Control Block

| Rev | Date | Changed By | Reviewed By | Approved By | Summary |
|-----|------|------------|-------------|-------------|---------|
| 0.9 | 2026-06-06 | jschwefel | — | — | Initial release |
