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
45 changes: 26 additions & 19 deletions .github/workflows/docker.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: Docker image

# Builds and publishes the public Buzz relay images as ghcr.io/block/buzz.
# Builds and publishes Launchpad Buzz relay images as ghcr.io/launchpad-26/buzz.
# Normal tags contain stripped binaries; matching debug-* tags contain the same
# optimized binaries with line-table debug information for native profilers.
#
Expand All @@ -16,8 +16,8 @@ name: Docker image
# the relay image version tracks crates/buzz-relay/Cargo.toml, never desktop.
#
# Triggers:
# - push to main → :main + :sha-<7>
# + :debug-main + :debug-sha-<7>
# - push to launchpad → :launchpad + :sha-<full-commit>
# + :debug-launchpad + :debug-sha-<full-commit>
# - push tags relay-v*.*.* → :{version} + :{major}.{minor} + :{major}
# + matching :debug-* tags
# (+ :latest/:debug-latest for stable releases)
Expand All @@ -37,11 +37,11 @@ name: Docker image
# The :latest tag tracks the latest STABLE relay release: metadata-action's
# `flavor.latest=auto` (its default) emits :latest only for non-prerelease
# semver, so relay-v0.3.0-rc.1 publishes :0.3.0-rc.1 without moving :latest,
# and main pushes (no semver tag) never produce :latest.
# and launchpad pushes (no semver tag) never produce :latest.

on:
push:
branches: [main]
branches: [launchpad]
tags: ["relay-v[0-9]*"]
pull_request:
paths:
Expand All @@ -65,18 +65,16 @@ on:
required: true

# One image build per ref; cancel superseded PR builds, but never cancel
# tag/main builds (publishing must not be aborted mid-flight).
# tag/launchpad builds (publishing must not be aborted mid-flight).
concurrency:
group: docker-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref_type == 'branch' && github.event_name == 'pull_request' }}

permissions: {}

env:
# Single source of truth for the image name. Set GHCR_IMAGE as a repo
# variable to override (e.g., for forks that want to push to their own
# namespace without forking this file).
IMAGE_NAME: ${{ vars.GHCR_IMAGE != '' && vars.GHCR_IMAGE || 'ghcr.io/block/buzz' }}
# Launchpad publication must never fall back to the upstream Block package.
IMAGE_NAME: ghcr.io/launchpad-26/buzz

jobs:
build:
Expand Down Expand Up @@ -138,29 +136,35 @@ jobs:
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
with:
images: ${{ env.IMAGE_NAME }}
# Tag matrix — every main commit gets sha-<7>, relay releases get the
# full semver family. The semver entries carry match=^relay-v(.*)$
# Tag matrix — every build gets a full commit-SHA tag, while relay
# releases also get the semver family. The sha tag carries no
# enable= gate, deliberately: a workflow_dispatch rescue still needs
# a commit-pinned tag for launchpad/deploy/run.sh's guard to accept,
# so it publishes on every event including a rescue dispatch. The
# semver entries carry match=^relay-v(.*)$
# because metadata-action does NOT strip a `relay-v` prefix on its
# own — it only strips refs/tags/, then runs the raw ref through
# semver.valid(), which rejects "relay-v0.3.0". The match capture
# group feeds the bare version to the semver parser. value= supplies
# the version on a manual rescue dispatch (github.ref is `main`
# the version on a manual rescue dispatch (github.ref is `launchpad`
# there, not the tag): it is already bare, so match no-ops (warns,
# value intact) and the bare version validates as-is. On push value=
# is empty, so the ref drives it and match strips relay-v — push
# behavior is unchanged. Pull requests get nothing (push: false
# below). :latest is intentionally absent — flavor.latest defaults to
# `auto`, which adds :latest for stable semver tags only (not
# prereleases, not main pushes).
# prereleases, not launchpad pushes).
tags: |
type=ref,event=branch,enable=${{ github.event_name != 'workflow_dispatch' || inputs.version == '' }}
type=sha,prefix=sha-,format=short,enable=${{ github.event_name != 'workflow_dispatch' || inputs.version == '' }}
type=sha,prefix=sha-,format=long
type=semver,pattern={{version}},match=^relay-v(.*)$,value=${{ inputs.version }}
type=semver,pattern={{major}}.{{minor}},match=^relay-v(.*)$,value=${{ inputs.version }}
type=semver,pattern={{major}},match=^relay-v(.*)$,value=${{ inputs.version }}
labels: |
org.opencontainers.image.title=Buzz
org.opencontainers.image.description=WebSocket relay server for the Buzz communications platform
org.opencontainers.image.source=https://github.com/launchpad-26/buzz
org.opencontainers.image.revision=${{ github.sha }}
org.opencontainers.image.licenses=Apache-2.0

- name: Build and push release image by digest
Expand Down Expand Up @@ -275,7 +279,7 @@ jobs:
prefix=${{ matrix.tag_prefix }},onlatest=true
tags: |
type=ref,event=branch,enable=${{ github.event_name != 'workflow_dispatch' || inputs.version == '' }}
type=sha,prefix=${{ matrix.tag_prefix }}sha-,format=short,enable=${{ github.event_name != 'workflow_dispatch' || inputs.version == '' }}
type=sha,prefix=${{ matrix.tag_prefix }}sha-,format=long
type=semver,pattern={{version}},match=^relay-v(.*)$,value=${{ inputs.version }}
type=semver,pattern={{major}}.{{minor}},match=^relay-v(.*)$,value=${{ inputs.version }}
type=semver,pattern={{major}},match=^relay-v(.*)$,value=${{ inputs.version }}
Expand Down Expand Up @@ -310,7 +314,7 @@ jobs:

- name: Attest provenance for the merged image
# Sigstore-signed in-toto attestation, verifiable with:
# gh attestation verify oci://ghcr.io/block/buzz:<tag> --owner block
# gh attestation verify oci://ghcr.io/launchpad-26/buzz:<tag> --owner launchpad-26
uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1
with:
subject-name: ${{ env.IMAGE_NAME }}
Expand All @@ -336,12 +340,15 @@ jobs:
echo
echo "Verify provenance:"
echo '```'
echo "gh attestation verify oci://${IMAGE_NAME}@${MERGED_DIGEST} --owner block"
echo "gh attestation verify oci://${IMAGE_NAME}@${MERGED_DIGEST} --owner launchpad-26"
echo '```'
} >> "$GITHUB_STEP_SUMMARY"

push-gateway-build:
name: Build public push gateway (${{ matrix.platform }})
# Launchpad does not operate the separate APNs gateway. Preserve the
# inherited lane for upstream while preventing fork publication attempts.
if: github.repository == 'block/buzz'
runs-on: ${{ matrix.runner }}
timeout-minutes: 60
permissions:
Expand Down Expand Up @@ -419,7 +426,7 @@ jobs:

push-gateway-merge:
name: Publish public push gateway image
if: github.event_name != 'pull_request'
if: github.repository == 'block/buzz' && github.event_name != 'pull_request'
runs-on: ubuntu-24.04
needs: push-gateway-build
timeout-minutes: 15
Expand Down
8 changes: 4 additions & 4 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# syntax=docker/dockerfile:1.7
#
# Public Buzz relay image — published as ghcr.io/block/buzz:<tag>.
# Launchpad Buzz relay image — published as ghcr.io/launchpad-26/buzz:<tag>.
#
# Builds the `buzz-relay` binary (Rust 1.95) and the `buzz-web` static bundle
# (pnpm + vite), then assembles them into a small debian-slim runtime with
Expand Down Expand Up @@ -126,9 +126,9 @@ FROM debian:${DEBIAN_VERSION}-slim AS runtime-base
# one — without it GHCR keeps the image private even when the repo is public.
LABEL org.opencontainers.image.title="Buzz" \
org.opencontainers.image.description="WebSocket relay server for the Buzz communications platform" \
org.opencontainers.image.source="https://github.com/block/buzz" \
org.opencontainers.image.url="https://github.com/block/buzz" \
org.opencontainers.image.documentation="https://github.com/block/buzz#readme" \
org.opencontainers.image.source="https://github.com/launchpad-26/buzz" \
org.opencontainers.image.url="https://github.com/launchpad-26/buzz" \
org.opencontainers.image.documentation="https://github.com/launchpad-26/buzz#readme" \
org.opencontainers.image.licenses="Apache-2.0"

RUN apt-get update \
Expand Down
5 changes: 3 additions & 2 deletions deploy/compose/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
# Copy to .env and replace every CHANGE_ME value before running.
# The bootstrap script should generate this file for normal users.

# Image published by the public image pipeline. Use `:main` for pre-release testing. Pin `:sha-<7>` or a semver release tag for production.
BUZZ_IMAGE=ghcr.io/block/buzz:main
# Image published from the Launchpad `launchpad` branch. Production must use
# either a digest or the full 40-character commit-SHA tag from the workflow.
BUZZ_IMAGE=ghcr.io/launchpad-26/buzz:sha-CHANGE_ME_FULL_40_CHARACTER_GIT_COMMIT

# Public host name. Used by compose.caddy.yml and URL-derived settings below.
BUZZ_DOMAIN=buzz.example.com
Expand Down
74 changes: 61 additions & 13 deletions deploy/compose/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,23 @@ the root `docker-compose.yml`, which remains local development infrastructure.
```bash
cd deploy/compose
cp .env.example .env
$EDITOR .env # replace every CHANGE_ME value
./run.sh start
$EDITOR .env
cd ../..
```

In `.env`, replace every `CHANGE_ME` value and set `BUZZ_IMAGE` to either the
published image digest or the full 40-character commit-SHA tag. From the
repository root, validate and start through the Launchpad guard:

```bash
./launchpad/deploy/run.sh check
./launchpad/deploy/run.sh start
```

For a public VPS with automatic Let's Encrypt certificates:

```bash
cd deploy/compose
BUZZ_COMPOSE_TLS=true ./run.sh start
BUZZ_COMPOSE_TLS=true ./launchpad/deploy/run.sh start
```

The bootstrap script should eventually replace manual `.env` editing for normal
Expand All @@ -27,7 +35,14 @@ keypair.

- Requires Docker Compose v2.24.4 or newer; the TLS override uses Compose's
`!reset` tag to remove the direct relay port when Caddy terminates HTTPS.
- Default `BUZZ_IMAGE` tracks `ghcr.io/block/buzz:main` for early testing. Pin it to `ghcr.io/block/buzz:sha-<7>` or a semver release tag for production once available.
- GitHub Actions builds the root `Dockerfile` when a commit reaches the
`launchpad` branch and publishes `ghcr.io/launchpad-26/buzz`. Normal VPS
deployment consumes that prebuilt image; it does not build the Dockerfile.
- `BUZZ_IMAGE` has no default. Compose fails when it is missing. Production
should use `ghcr.io/launchpad-26/buzz@sha256:<digest>` or the workflow's
`ghcr.io/launchpad-26/buzz:sha-<full-40-character-commit>` tag. The moving
`:launchpad` tag is only a convenience pointer and is rejected by the
Launchpad guard unless explicitly allowed for development/testing.
- Keep `BUZZ_RELAY_PRIVATE_KEY`, `BUZZ_GIT_HOOK_HMAC_SECRET`, database/Redis,
and S3 secrets stable across restarts.
- `RELAY_OWNER_PUBKEY` is intentionally not prefixed with `BUZZ_`; it must be a
Expand All @@ -44,18 +59,51 @@ keypair.
`.env`; use the Helm chart or a custom Compose configuration for providers
such as new Railway Storage Buckets that require `virtual` addressing.

Run `./run.sh backup-hint` for the backup checklist.
Run `./launchpad/deploy/run.sh backup-hint` from the repository root for the
backup checklist.

## Image lifecycle and traceability

The command behavior is intentionally distinct:

- `start` runs `docker compose up -d --wait`. It does not explicitly pull; an
absent image may be fetched by Compose, while an already-present tag may be
reused.
- `upgrade` runs `docker compose pull` and then `up -d --wait`. Use this after
intentionally changing `BUZZ_IMAGE`.
- `restart` force-recreates only the relay and does not pull an image.

To see the configured image and the immutable digest of the running relay:

```bash
cd deploy/compose
docker compose config --images
container_id=$(docker compose ps -q relay)
docker inspect --format 'configured={{.Config.Image}} image_id={{.Image}}' "$container_id"
image_id=$(docker inspect --format '{{.Image}}' "$container_id")
docker image inspect --format 'repo_digests={{json .RepoDigests}}' "$image_id"
```

The full `sha-<40-character-commit>` tag maps directly to Git. A digest is the
strongest runtime pin; the corresponding workflow run summary records both the
digest and full commit-SHA tag and includes the provenance verification command.

To upgrade, replace only `BUZZ_IMAGE` in the local, untracked `.env` with the
new verified digest or full commit-SHA tag, run
`./launchpad/deploy/run.sh check`, back up state, and run
`./launchpad/deploy/run.sh upgrade`. To roll back, restore the previous
immutable `BUZZ_IMAGE` value and run the same check and upgrade commands. An
image-only rollback is safe only when intervening database migrations are
backward-compatible; otherwise restore the matching pre-upgrade database and
object/git snapshots as a coordinated recovery.

## Validation

Before sharing an install link publicly, verify a fresh install with:

```bash
cd deploy/compose
cp .env.example .env
$EDITOR .env
./run.sh config
./run.sh start
curl -fsS "http://127.0.0.1:$(grep -E '^BUZZ_HTTP_PORT=' .env | cut -d= -f2-)/_liveness"
./run.sh status
./launchpad/deploy/run.sh check
./launchpad/deploy/run.sh start
curl -fsS "http://127.0.0.1:$(grep -E '^BUZZ_HTTP_PORT=' deploy/compose/.env | cut -d= -f2-)/_liveness"
./launchpad/deploy/run.sh status
```
2 changes: 1 addition & 1 deletion deploy/compose/compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: buzz-prod

services:
relay:
image: ${BUZZ_IMAGE:-ghcr.io/block/buzz:main}
image: ${BUZZ_IMAGE:?BUZZ_IMAGE must be set to an immutable relay image}
env_file:
- .env
environment:
Expand Down
6 changes: 4 additions & 2 deletions launchpad/deploy/AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
# AGENTS.md — failed deployment archive
# AGENTS.md — Launchpad deployment guard and failed deployment archive

The deployment method formerly stored in this directory **failed** and has been
moved to `archived/` for historical reference only.
moved to `archived/` for historical reference only. The active `run.sh` in this
directory is a thin Launchpad image guard that delegates all orchestration to
`../../deploy/compose/run.sh`; it is not a replacement Compose implementation.

## Mandatory rule

Expand Down
24 changes: 23 additions & 1 deletion launchpad/deploy/README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,26 @@
# Failed deployment method — archived
# Launchpad VPS deployment guard

Use [`run.sh`](run.sh) for Launchpad VPS operations. It validates that
`deploy/compose/.env` selects an immutable `ghcr.io/launchpad-26/buzz` relay
image, rejects upstream Block images, checks Docker Compose compatibility, and
then delegates to the canonical `deploy/compose/run.sh` implementation.

```bash
./launchpad/deploy/run.sh check
./launchpad/deploy/run.sh start
./launchpad/deploy/run.sh upgrade
```

Digest references and full 40-character `sha-...` tags are accepted for normal
deployment. Floating tags are rejected unless
`BUZZ_ALLOW_FLOATING_IMAGE=true` is explicitly set for development or testing.
The override never permits `ghcr.io/block/buzz`.

The guard reads exactly one `BUZZ_IMAGE` assignment from the local `.env` and
exports that value before delegation, so an ambient shell variable cannot
silently replace the reviewed deployment image.

## Failed deployment method — archived

Everything from the former Launchpad VPS deployment experiment has been moved
to `archived/` for future reference.
Expand Down
Loading
Loading