Context
We need a staging environment to safely test changes before production. Staging will be gated with Caddy basic auth and use dedicated R2 buckets.
Step 1: Caddyfile — Add Conditional Basic Auth
File: deploy/ansible/roles/caddy/templates/Caddyfile.j2
Add a conditional basicauth * block. Production never defines caddy_basic_auth_enabled, so it's unaffected.
{% if caddy_basic_auth_enabled | default(false) %}
basicauth * {
{{ caddy_basic_auth_user }} {{ caddy_basic_auth_hash }}
}
{% endif %}
Caddy uses bcrypt hashes natively — generate with caddy hash-password --plaintext '<password>'.
For Composer client testing, configure auth.json:
{"http-basic": {"staging.wp-packages.org": {"username": "staging", "password": "<pass>"}}}
Step 2: Ansible Multi-Environment Setup
2a. Update ansible.cfg
File: deploy/ansible/ansible.cfg
Remove inventory = inventory/hosts/production.yml. Inventory will be passed explicitly via -i flag to prevent accidental production deploys.
2b. Create staging inventory example
New file: deploy/ansible/inventory/hosts/staging.example.yml
all:
hosts:
wp-packages-staging:
ansible_host: your.staging.server.ip
ansible_user: deploy
ansible_python_interpreter: /usr/bin/python3
2c. Create staging group_vars
New file: deploy/ansible/group_vars/staging/main.yml
Copy from production with these overrides:
app_domain: staging.wp-packages.org
app_env: staging
app_debug: "true"
go_log_level: debug
r2_cdn_public_url: "https://cdn-staging.wp-packages.org"
caddy_basic_auth_enabled: true
caddy_basic_auth_user: "staging"
caddy_basic_auth_hash: "{{ vault_caddy_basic_auth_hash }}"
Everything else (timers, users, litestream version, etc.) stays the same as production.
New file: deploy/ansible/group_vars/staging/vault.example.yml
Same as production vault.example.yml plus vault_caddy_basic_auth_hash: "REPLACE_ME".
Step 3: GitHub Actions Workflow
File: .github/workflows/deploy.yml
Add an environment input (choice: staging/production, default: staging):
concurrency: ${{ inputs.environment }}-deploy — independent locks per env
- Conditionally use
PROD_* or STAGING_* secrets based on the selected environment
- Pass
-i inventory/hosts/${{ inputs.environment }}.yml explicitly to ansible-playbook
GitHub repo setup (manual, one-time):
Add repo-level secrets: STAGING_SSH_PRIVATE_KEY, STAGING_ANSIBLE_VAULT_PASSWORD, STAGING_INVENTORY_YML_B64, STAGING_VAULT_YML_B64
Step 4: R2 Buckets (Manual in Cloudflare)
Create three staging buckets (using existing R2 API token):
wp-packages-staging — Composer package metadata
wp-packages-staging-cdn — CDN assets, with custom domain cdn-staging.wp-packages.org
wp-packages-staging-backups — Litestream SQLite replicas
Step 5: DNS & Cloudflare (Manual)
staging.wp-packages.org — A record to staging server IP (proxied through Cloudflare)
cdn-staging.wp-packages.org — set up via R2 custom domain settings
- Generate a separate Cloudflare origin certificate for
staging.wp-packages.org
- Store staging cert + key in staging vault (
vault_ssl_certificate, vault_ssl_private_key)
Step 6: Provision & Verify
- Spin up staging VPS
- Add all
STAGING_* secrets to the GitHub repo
- Run workflow:
environment: staging, action: provision, ref: main
- Verify:
curl https://staging.wp-packages.org → 401 (basic auth required)
curl -u staging:<pass> https://staging.wp-packages.org → 200
- R2 uploads land in staging buckets
composer require wp-plugin/akismet works with auth.json configured
Files to Create/Modify
| File |
Action |
deploy/ansible/roles/caddy/templates/Caddyfile.j2 |
Add conditional basicauth block |
deploy/ansible/ansible.cfg |
Remove hardcoded inventory line |
deploy/ansible/group_vars/staging/main.yml |
Create — staging config |
deploy/ansible/group_vars/staging/vault.example.yml |
Create — vault key reference |
deploy/ansible/inventory/hosts/staging.example.yml |
Create — inventory example |
.github/workflows/deploy.yml |
Add environment input, conditional secrets, per-env concurrency |
docs/operations.md |
Update for multi-environment support (inventory -i flag, staging secrets, etc.) |
Follow up after #28.
Context
We need a staging environment to safely test changes before production. Staging will be gated with Caddy basic auth and use dedicated R2 buckets.
Step 1: Caddyfile — Add Conditional Basic Auth
File:
deploy/ansible/roles/caddy/templates/Caddyfile.j2Add a conditional
basicauth *block. Production never definescaddy_basic_auth_enabled, so it's unaffected.Caddy uses bcrypt hashes natively — generate with
caddy hash-password --plaintext '<password>'.For Composer client testing, configure
auth.json:{"http-basic": {"staging.wp-packages.org": {"username": "staging", "password": "<pass>"}}}Step 2: Ansible Multi-Environment Setup
2a. Update ansible.cfg
File:
deploy/ansible/ansible.cfgRemove
inventory = inventory/hosts/production.yml. Inventory will be passed explicitly via-iflag to prevent accidental production deploys.2b. Create staging inventory example
New file:
deploy/ansible/inventory/hosts/staging.example.yml2c. Create staging group_vars
New file:
deploy/ansible/group_vars/staging/main.ymlCopy from production with these overrides:
Everything else (timers, users, litestream version, etc.) stays the same as production.
New file:
deploy/ansible/group_vars/staging/vault.example.ymlSame as production vault.example.yml plus
vault_caddy_basic_auth_hash: "REPLACE_ME".Step 3: GitHub Actions Workflow
File:
.github/workflows/deploy.ymlAdd an
environmentinput (choice: staging/production, default: staging):concurrency: ${{ inputs.environment }}-deploy— independent locks per envPROD_*orSTAGING_*secrets based on the selected environment-i inventory/hosts/${{ inputs.environment }}.ymlexplicitly to ansible-playbookGitHub repo setup (manual, one-time):
Add repo-level secrets:
STAGING_SSH_PRIVATE_KEY,STAGING_ANSIBLE_VAULT_PASSWORD,STAGING_INVENTORY_YML_B64,STAGING_VAULT_YML_B64Step 4: R2 Buckets (Manual in Cloudflare)
Create three staging buckets (using existing R2 API token):
wp-packages-staging— Composer package metadatawp-packages-staging-cdn— CDN assets, with custom domaincdn-staging.wp-packages.orgwp-packages-staging-backups— Litestream SQLite replicasStep 5: DNS & Cloudflare (Manual)
staging.wp-packages.org— A record to staging server IP (proxied through Cloudflare)cdn-staging.wp-packages.org— set up via R2 custom domain settingsstaging.wp-packages.orgvault_ssl_certificate,vault_ssl_private_key)Step 6: Provision & Verify
STAGING_*secrets to the GitHub repoenvironment: staging,action: provision,ref: maincurl https://staging.wp-packages.org→ 401 (basic auth required)curl -u staging:<pass> https://staging.wp-packages.org→ 200composer require wp-plugin/akismetworks with auth.json configuredFiles to Create/Modify
deploy/ansible/roles/caddy/templates/Caddyfile.j2deploy/ansible/ansible.cfgdeploy/ansible/group_vars/staging/main.ymldeploy/ansible/group_vars/staging/vault.example.ymldeploy/ansible/inventory/hosts/staging.example.yml.github/workflows/deploy.ymldocs/operations.md-iflag, staging secrets, etc.)Follow up after #28.