deploy relay #22
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |