Skip to content

deploy relay

deploy relay #22

Workflow file for this run

name: deploy relay
# Ops controls for the CI relay (moqx-main.ci.openmoq.org + moqx-000 alias).
# Restart, redeploy with a specific image tag, or change verbose level.
on:
workflow_dispatch:
inputs:
restart_only:
description: "Restart container without redeploying"
required: false
type: boolean
default: false
image_tag:
description: "Docker image tag (default: derived from branch — main→latest, release/X→X)"
required: false
default: ""
type: string
domain:
description: "DNS hostname (default: derived from branch — main→moqx-main, release/X→moqx-X)"
required: false
default: ""
type: string
verbose:
description: "GLOG verbose level (0=off, 1-3=increasing detail)"
required: false
default: "0"
type: choice
options:
- "0"
- "1"
- "2"
- "3"
permissions:
contents: read
packages: read
env:
RELAY_PORT: 4433
ADMIN_PORT: 8000
jobs:
deploy:
runs-on: [self-hosted, linode]
steps:
- uses: actions/checkout@v4
- name: Compute deployment target
id: target
run: |
# Branch label: main → "main"; release/<name> → "<name>"
# Rolling Docker tag: <label>-latest (symmetric across branches)
BRANCH="${{ github.ref_name }}"
if [[ "$BRANCH" == "main" ]]; then
LABEL="main"
else
LABEL="${BRANCH#release/}"
fi
DEFAULT_TAG="${LABEL}-latest"
# Explicit input overrides, otherwise computed defaults
IMAGE_TAG="${{ inputs.image_tag }}"
IMAGE_TAG="${IMAGE_TAG:-$DEFAULT_TAG}"
DOMAIN="${{ inputs.domain }}"
DOMAIN="${DOMAIN:-moqx-${LABEL}.ci.openmoq.org}"
echo "image_tag=$IMAGE_TAG" >> "$GITHUB_OUTPUT"
echo "domain=$DOMAIN" >> "$GITHUB_OUTPUT"
echo "Deploying ghcr.io/${{ github.repository }}:${IMAGE_TAG} → ${DOMAIN}"
- name: Ensure DNS A record
if: ${{ !inputs.restart_only }}
env:
DOMAIN: ${{ steps.target.outputs.domain }}
AWS_ACCESS_KEY_ID: ${{ secrets.OMOQ_CERTBOT_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.OMOQ_CERTBOT_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: us-east-1
HOSTED_ZONE_ID: Z0079758316CI99B99FLR
run: |
# Discover the public IP of this self-hosted runner (the deploy target)
IP=$(curl -sf https://checkip.amazonaws.com | tr -d '[:space:]')
if [[ -z "$IP" ]]; then
echo "::error::Could not determine runner public IP"
exit 1
fi
echo "Runner public IP: $IP"
echo "Ensuring A record: $DOMAIN → $IP"
# Check current record (if any). UPSERT is idempotent — only changes
# if the record is missing or pointing elsewhere.
CHANGE_FILE=$(mktemp)
cat > "$CHANGE_FILE" <<EOF
{
"Comment": "moqx deploy: $DOMAIN",
"Changes": [{
"Action": "UPSERT",
"ResourceRecordSet": {
"Name": "$DOMAIN",
"Type": "A",
"TTL": 300,
"ResourceRecords": [{"Value": "$IP"}]
}
}]
}
EOF
CHANGE_ID=$(aws route53 change-resource-record-sets \
--hosted-zone-id "$HOSTED_ZONE_ID" \
--change-batch "file://$CHANGE_FILE" \
--query 'ChangeInfo.Id' --output text)
echo "Route53 change submitted: $CHANGE_ID"
# Wait for the change to propagate (usually seconds). Bounded wait
# so a slow Route53 doesn't hang the deploy forever.
echo "Waiting for DNS change to propagate..."
aws route53 wait resource-record-sets-changed --id "$CHANGE_ID"
echo "DNS change INSYNC"
rm -f "$CHANGE_FILE"
- name: Log in to GHCR
run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
- name: Run
working-directory: docker
env:
RESTART_ONLY: ${{ inputs.restart_only }}
VERBOSE: ${{ inputs.verbose }}
DOMAIN: ${{ steps.target.outputs.domain }}
IMAGE_TAG: ${{ steps.target.outputs.image_tag }}
AWS_ACCESS_KEY_ID: ${{ secrets.OMOQ_CERTBOT_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.OMOQ_CERTBOT_SECRET_ACCESS_KEY }}
run: |
if [ "$RESTART_ONLY" = "true" ]; then
echo "==> Restarting relay..."
docker compose restart moqx
else
# Write .env
cat > .env <<EOF
DOMAIN=${DOMAIN}
CERTBOT_EMAIL=gmarzot@openmoq.org
MOQX_PORT=${RELAY_PORT}
MOQX_ADMIN_PORT=${ADMIN_PORT}
MOQX_LOG_LEVEL=0
MOQX_VERBOSE=${VERBOSE}
EOF
sed -i 's/^[[:space:]]*//' .env
# Ensure TLS cert
CERT="/etc/letsencrypt/live/${DOMAIN}/fullchain.pem"
if [ ! -f "$CERT" ]; then
echo "::warning::No cert for ${DOMAIN} — provisioning via Route53"
sudo -E certbot certonly --dns-route53 \
-d "$DOMAIN" --non-interactive --agree-tos \
--email gmarzot@openmoq.org
elif ! sudo openssl x509 -in "$CERT" -noout -checkend 2592000 2>/dev/null; then
echo "::warning::Cert for ${DOMAIN} expires within 30 days — renewing"
sudo -E certbot renew --cert-name "$DOMAIN"
else
echo "Cert for ${DOMAIN} is valid"
fi
# Pull and deploy
docker pull "ghcr.io/${{ github.repository }}:${IMAGE_TAG}"
IMAGE="ghcr.io/${{ github.repository }}:${IMAGE_TAG}"
if [ "$IMAGE_TAG" != "latest" ]; then
docker tag "$IMAGE" "ghcr.io/${{ github.repository }}:latest"
fi
echo "==> Stopping existing container (if any)..."
docker compose down --remove-orphans 2>/dev/null || true
docker rm -f moqx logmon 2>/dev/null || true
echo "==> Starting relay on ${DOMAIN}:${RELAY_PORT}..."
docker compose up -d
fi
# Health check (both paths)
echo "==> Waiting for admin endpoint..."
for i in $(seq 1 30); do
if curl -sf http://127.0.0.1:${ADMIN_PORT}/info >/dev/null 2>&1; then
break
fi
sleep 1
if [ "$i" -eq 30 ]; then
echo "::error::Admin endpoint did not respond within 30s"
docker compose logs moqx
exit 1
fi
done
RESP=$(curl -sf http://127.0.0.1:${ADMIN_PORT}/info)
echo "==> Relay running: $RESP"
- name: Notify Slack
if: always()
env:
SLACK_WEBHOOK_URL: ${{ secrets.OMOQ_SLACK_WEBHOOK_URL }}
DOMAIN: ${{ steps.target.outputs.domain }}
IMAGE_TAG: ${{ steps.target.outputs.image_tag }}
run: |
SHORT="${GITHUB_SHA:0:7}"
ACTION=${{ inputs.restart_only && '"restarted"' || '"deployed"' }}
STATUS=${{ job.status == 'success' && '":rocket:"' || '":x:"' }}
TEXT="${STATUS} *${{ github.repository }}* ${ACTION} \`${IMAGE_TAG}\` on \`${DOMAIN}:${RELAY_PORT}\`"
curl -sf -X POST "$SLACK_WEBHOOK_URL" \
-H "Content-Type: application/json" \
--data "{\"text\": \"${TEXT}\"}" || true