Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
187 changes: 187 additions & 0 deletions .github/workflows/dcm-production-deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
name: DCM production deploy

on:
pull_request:
branches: [dcm-production]
paths:
- ".github/workflows/dcm-production-deploy.yml"
- "deploy/dcm/**"
- "AGENTS.md"
- "docs/DCM_PRODUCTION_DEPLOYMENT.md"
- "docs/DIVINE_CREATIVE_FORK_WORKFLOW.md"
push:
branches: [dcm-production]
workflow_dispatch:

concurrency:
group: dcm-production-deploy
cancel-in-progress: false

permissions: {}

env:
IMAGE_NAME: ghcr.io/divine-creative-ministries/buzz

jobs:
validate:
name: Validate DCM deployment files
runs-on: ubuntu-24.04
timeout-minutes: 10
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
persist-credentials: false

- name: Validate scripts
run: deploy/dcm/tests/test-deploy-scripts.sh

build:
name: Build immutable production image
if: github.event_name != 'pull_request'
needs: validate
runs-on: ubuntu-24.04
timeout-minutes: 90
permissions:
contents: read
packages: write
id-token: write
attestations: write
outputs:
digest: ${{ steps.build.outputs.digest }}
steps:
- name: Require the production branch
env:
SOURCE_REF: ${{ github.ref }}
run: |
if [[ "$SOURCE_REF" != "refs/heads/dcm-production" ]]; then
echo "::error::Production builds must use dcm-production; got $SOURCE_REF"
exit 1
fi

- name: Checkout exact production commit
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
fetch-depth: 0
persist-credentials: false

- name: Verify checked-out commit
run: test "$(git rev-parse HEAD)" = "$GITHUB_SHA"

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
with:
buildkitd-config-inline: |
[worker.oci]
max-parallelism = 2

- name: Log in to GHCR
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Build and publish exact commit
id: build
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
with:
context: .
file: ./Dockerfile
target: runtime
platforms: linux/amd64
push: true
tags: ${{ env.IMAGE_NAME }}:dcm-${{ github.sha }}
labels: |
org.opencontainers.image.title=Divine Creative Buzz
org.opencontainers.image.description=Divine Creative Ministries production Buzz relay
org.opencontainers.image.licenses=Apache-2.0
org.opencontainers.image.revision=${{ github.sha }}
org.opencontainers.image.source=https://github.com/${{ github.repository }}
cache-from: type=registry,ref=${{ env.IMAGE_NAME }}-buildcache:amd64
cache-to: type=registry,ref=${{ env.IMAGE_NAME }}-buildcache:amd64,mode=max,compression=zstd

- name: Attest image provenance
uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1
with:
subject-name: ${{ env.IMAGE_NAME }}
subject-digest: ${{ steps.build.outputs.digest }}
push-to-registry: true

- name: Verify published provenance
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
IMAGE_DIGEST: ${{ steps.build.outputs.digest }}
run: gh attestation verify "oci://${IMAGE_NAME}@${IMAGE_DIGEST}" --repo "$GITHUB_REPOSITORY"

- name: Record immutable image
env:
IMAGE_DIGEST: ${{ steps.build.outputs.digest }}
run: |
{
echo "### DCM production image"
echo
echo "Commit: \`$GITHUB_SHA\`"
echo "Image: \`${IMAGE_NAME}@${IMAGE_DIGEST}\`"
} >> "$GITHUB_STEP_SUMMARY"

deploy:
name: Deploy to buzz.divinecreative.org
if: needs.build.result == 'success' && vars.DCM_DEPLOY_PAUSED != 'true'
needs: build
runs-on: ubuntu-24.04
timeout-minutes: 30
environment:
name: production
url: https://buzz.divinecreative.org
permissions:
contents: read
steps:
- name: Configure restricted SSH identity
env:
DEPLOY_KEY: ${{ secrets.DCM_DEPLOY_SSH_KEY }}
KNOWN_HOSTS: ${{ vars.DCM_DEPLOY_KNOWN_HOSTS }}
run: |
install -m 700 -d "$HOME/.ssh"
printf '%s\n' "$DEPLOY_KEY" > "$HOME/.ssh/dcm_deploy"
chmod 600 "$HOME/.ssh/dcm_deploy"
printf '%s\n' "$KNOWN_HOSTS" > "$HOME/.ssh/known_hosts"
chmod 600 "$HOME/.ssh/known_hosts"
ssh-keygen -y -f "$HOME/.ssh/dcm_deploy" >/dev/null
ssh-keygen -l -f "$HOME/.ssh/known_hosts"

- name: Request digest-pinned deployment
env:
DEPLOY_HOST: ${{ vars.DCM_DEPLOY_HOST }}
DEPLOY_PORT: ${{ vars.DCM_DEPLOY_PORT }}
DEPLOY_USER: ${{ vars.DCM_DEPLOY_USER }}
IMAGE_DIGEST: ${{ needs.build.outputs.digest }}
run: |
: "${DEPLOY_HOST:?missing DCM_DEPLOY_HOST}"
: "${DEPLOY_PORT:?missing DCM_DEPLOY_PORT}"
: "${DEPLOY_USER:?missing DCM_DEPLOY_USER}"
: "${IMAGE_DIGEST:?missing image digest}"
ssh \
-i "$HOME/.ssh/dcm_deploy" \
-p "$DEPLOY_PORT" \
-o BatchMode=yes \
-o IdentitiesOnly=yes \
-o StrictHostKeyChecking=yes \
-o UserKnownHostsFile="$HOME/.ssh/known_hosts" \
-o ConnectTimeout=20 \
"$DEPLOY_USER@$DEPLOY_HOST" \
"deploy $GITHUB_SHA ${IMAGE_NAME}@${IMAGE_DIGEST}"

- name: Deployment summary
env:
IMAGE_DIGEST: ${{ needs.build.outputs.digest }}
run: |
{
echo "### Production deployment passed"
echo
echo "URL: https://buzz.divinecreative.org"
echo "Commit: \`$GITHUB_SHA\`"
echo "Image: \`${IMAGE_NAME}@${IMAGE_DIGEST}\`"
} >> "$GITHUB_STEP_SUMMARY"
59 changes: 59 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,65 @@ code style, PR process, architecture), see [CONTRIBUTING.md](CONTRIBUTING.md).

---

## Divine Creative Fork Policy

These rules apply when the working repository is
`Divine-Creative-Ministries/buzz`, or when work is based on that fork's
`dcm-production` branch. Before syncing upstream, changing branch strategy,
releasing, or deploying, read
[docs/DIVINE_CREATIVE_FORK_WORKFLOW.md](docs/DIVINE_CREATIVE_FORK_WORKFLOW.md).

- Treat the fork's `main` branch as an exact, fast-forward-only mirror of
`block/buzz:main`. Never commit Divine Creative changes to it.
- Put Divine Creative changes on short-lived `agent/*` branches and open pull
requests into `dcm-production`. Do not push directly to `main` or
`dcm-production`, and never force-push either branch.
- Fetch both `origin` and `upstream` before selecting a base or syncing. Branch
general-purpose upstream contributions from `upstream/main`; do not include
private integrations, deployment configuration, or Divine Creative branding
in those branches.
- Before merging a new upstream version into `dcm-production`, create and push
a recovery tag for the current production commit. Perform the merge on a
short-lived sync branch and merge it through a reviewed pull request.
- Resolve conflicts by understanding both changes. Never use blanket
`ours`/`theirs`, "accept all," history rewriting, or change deletion merely
to make a merge pass. Explain intentional removals in the pull request.
- Run the repository quality gates and relevant client/integration tests after
every upstream sync. A textually clean merge can still have behavioral or
schema conflicts.
- Never develop by editing `/opt/buzz`, running containers, or production data
on the VPS. Build from a reviewed commit, deploy an immutable image, record
its source SHA and digest, back up state first, and retain a tested rollback
path.
- Treat every merge into `dcm-production` as authorization for an immediate
automatic deployment to `buzz.divinecreative.org`. Read
[docs/DCM_PRODUCTION_DEPLOYMENT.md](docs/DCM_PRODUCTION_DEPLOYMENT.md) before
changing workflows, deployment assets, Compose behavior, migrations, or
production configuration. Do not merge work that is not production-ready.
- Preserve the restricted deployment boundary: GitHub may request only a full
commit SHA and digest-pinned image through the forced-command `buzzdeploy`
key. Never grant that key a shell, broaden its sudo rule, use a floating
image tag, expose secrets, remove deployment serialization, or bypass image
verification, backup, health, migration, or rollback checks.
- Set `DCM_DEPLOY_PAUSED=true` before merging a change that requires a
maintenance window, uninstalled production configuration, or reviewed
migration recovery. Root-owned deployment tooling on the VPS is updated only
through a separate operator action; it never self-modifies from CI.
- Distribute the customized production iOS app as a **Private Custom App** in
App Store Connect, restricted to the Divine Creative organization in Apple
Business and delivered through Apple Business or MDM. TestFlight is for
temporary testing only; development, Ad Hoc, Enterprise, unlisted, and public
App Store distribution are not production substitutes unless an explicit,
documented exception is approved. See the iOS policy in the fork workflow.
- Never commit credentials, `.env` contents, private keys, backup archives, or
production data. A merge to `dcm-production` authorizes application image
deployment only; it does not authorize unrelated infrastructure changes or
replacement of the root-owned deployment trust boundary.
- If the requested action would violate these rules, or the repository/branch
state is ambiguous, stop and ask for direction instead of guessing.

---

## Ecosystem

Buzz spans five repos. This one (`block/buzz`) is the OSS source for the relay, desktop, mobile, and CLI. The others handle internal builds and deployment:
Expand Down
46 changes: 46 additions & 0 deletions deploy/dcm/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# DCM Production Deployment Assets

These files implement the production boundary documented in
[`docs/DCM_PRODUCTION_DEPLOYMENT.md`](../../docs/DCM_PRODUCTION_DEPLOYMENT.md).
They are maintained in Git, but installation or replacement of root-owned VPS
deployment tooling is always a separate operator action.

## Installed Layout

| Repository file | VPS destination |
| --- | --- |
| `bin/buzz-deploy` | `/usr/local/sbin/buzz-deploy` |
| `bin/buzz-deploy-gate` | `/usr/local/libexec/buzz-deploy-gate` |
| `bin/buzzctl` | `/usr/local/sbin/buzzctl` |
| `compose/compose.pairing.yml` | `/etc/buzz/compose.pairing.yml` |
| `compose/Caddyfile` | `/etc/buzz/Caddyfile` |

All installed files are root-owned and not writable by the `buzzdeploy` user.
The production environment stays at `/opt/buzz/deploy/compose/.env`, mode
`0600`, and is never copied to GitHub.

The `buzzdeploy` SSH key must use a forced command:

```text
restrict,command="/usr/local/libexec/buzz-deploy-gate" ssh-ed25519 <public-key> dcm-production-deploy
```

The only permitted request is:

```text
deploy <40-character-commit-sha> ghcr.io/divine-creative-ministries/buzz@sha256:<64-character-digest>
```

The gate validates the request and may invoke only the root-owned deployment
script through a narrow passwordless sudo rule. The GitHub key cannot obtain an
interactive shell, forward ports, choose another image registry, request a
rollback, or read production secrets.

## Validation

```bash
deploy/dcm/tests/test-deploy-scripts.sh
```

The test performs Bash syntax checks, ShellCheck, one accepted gate request,
and rejection tests for malformed or overprivileged requests.
Loading
Loading