diff --git a/.mokogitea/workflows/deploy-rc.yml b/.mokogitea/workflows/deploy-rc.yml new file mode 100644 index 0000000000..2ad46f9cf4 --- /dev/null +++ b/.mokogitea/workflows/deploy-rc.yml @@ -0,0 +1,132 @@ +# Copyright (C) 2026 Moko Consulting +# SPDX-License-Identifier: GPL-3.0-or-later +# BRIEF: Build and deploy to the RC environment on push to the rc branch. +# Portable across Go server repos: ALL deployment config comes from repo +# Actions variables (vars.*) and secrets (secrets.*), nothing hardcoded. +# The rc branch is created by promote-rc when a PR to main opens. +# OWNER: Template-Go (canonical source; syncs to the root workflows dir). See Template-Go#3. +# +# Required repo VARIABLES (all tier-scoped so each environment is independent — +# a repo may host its rc/dev/prod tiers on different machines): +# RC_SSH_HOST, RC_SSH_PORT, RC_SSH_USERNAME - SSH deploy target for the rc tier +# RC_REGISTRY, RC_REGISTRY_USER, RC_IMAGE - container registry + login user + image +# RC_CONTAINER - compose service/container to recreate +# RC_COMPOSE_PROJECT - docker compose -p project name +# RC_COMPOSE_DIR - dir containing docker-compose.yml on host +# RC_SOURCE_DIR - build source checkout on host +# RC_TAG_ENV - compose env-var name that pins the image tag +# RC_HEALTH_URL - external URL to verify after deploy +# Required SECRETS (already configured org-wide; reused, not re-set): +# DEPLOY_SSH_KEY - deploy private key (repo/org secret) +# MOKOGITEA_TOKEN - registry/API token (org secret) + +name: Deploy (RC) + +on: + push: + branches: + - rc + +concurrency: + group: deploy-rc + cancel-in-progress: true + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +jobs: + deploy-rc: + name: "Build & Deploy to RC" + runs-on: ubuntu-latest + steps: + - name: Checkout source + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Determine version + id: config + run: | + VERSION=$(git describe --tags --always 2>/dev/null || echo "rc-$(git rev-parse --short HEAD)") + echo "tag=${VERSION}-rc" >> $GITHUB_OUTPUT + echo "Version: ${VERSION}-rc" + + - name: Write deploy key + env: + DEPLOY_KEY: ${{ secrets.DEPLOY_SSH_KEY }} + run: | + mkdir -p ~/.ssh + echo "$DEPLOY_KEY" > ~/.ssh/deploy_key + chmod 600 ~/.ssh/deploy_key + + - name: Build and deploy to RC via SSH + env: + REGISTRY_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} + TAG: ${{ steps.config.outputs.tag }} + run: | + # Runner-side values (TAG, REGISTRY_TOKEN) are injected into the remote shell + # via an env prefix; a *quoted* heredoc keeps every $var expanding once, on the + # remote. Repo variables (vars.*) are substituted inline by Actions before ssh. + ssh -i ~/.ssh/deploy_key -p ${{ vars.RC_SSH_PORT }} \ + -o ConnectTimeout=30 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \ + -o ServerAliveInterval=30 -o ServerAliveCountMax=10 \ + ${{ vars.RC_SSH_USERNAME }}@${{ vars.RC_SSH_HOST }} \ + "TAG='$TAG' REGISTRY_TOKEN='$REGISTRY_TOKEN' bash -s" <<'DEPLOY_EOF' + set -e + echo 'SSH connected to RC environment' + + if [ -z "$TAG" ]; then + echo 'ERROR: TAG is empty; refusing to build an untagged image' >&2 + exit 1 + fi + + HEALTH_FMT='{{.State.Health.Status}}' + + echo 'Cleaning Docker build cache...' + docker builder prune -af 2>/dev/null || true + docker image prune -af 2>/dev/null || true + + echo 'Pulling source...' + SOURCE_DIR='${{ vars.RC_SOURCE_DIR }}' + if [ ! -d "$SOURCE_DIR/.git" ]; then + git clone -b rc ${{ github.server_url }}/${{ github.repository }}.git "$SOURCE_DIR" + fi + cd "$SOURCE_DIR" + git remote set-url origin ${{ github.server_url }}/${{ github.repository }}.git 2>/dev/null || true + git fetch origin rc + git reset --hard origin/rc + + echo "Building image: ${{ vars.RC_REGISTRY }}/${{ vars.RC_IMAGE }}:$TAG" + docker build --no-cache --build-arg GOFLAGS='-p 1' \ + --tag "${{ vars.RC_REGISTRY }}/${{ vars.RC_IMAGE }}:$TAG" \ + -f Dockerfile . + + echo 'Pushing to registry...' + echo "$REGISTRY_TOKEN" | docker login ${{ vars.RC_REGISTRY }} -u ${{ vars.RC_REGISTRY_USER }} --password-stdin + docker push "${{ vars.RC_REGISTRY }}/${{ vars.RC_IMAGE }}:$TAG" + + echo 'Restarting RC container...' + cd '${{ vars.RC_COMPOSE_DIR }}' + # Drive the rc service image tag via its compose env-var (no sed on the shared + # file); remove any lingering fixed-name container first, then force-recreate. + docker rm -f '${{ vars.RC_CONTAINER }}' 2>/dev/null || true + ${{ vars.RC_TAG_ENV }}="$TAG" docker compose -p '${{ vars.RC_COMPOSE_PROJECT }}' up -d --force-recreate '${{ vars.RC_CONTAINER }}' + + echo 'Health check...' + for i in 1 2 3 4 5 6 7 8; do + sleep 15 + if docker inspect --format="$HEALTH_FMT" '${{ vars.RC_CONTAINER }}' 2>/dev/null | grep -q healthy; then + echo 'RC container healthy!' + exit 0 + fi + echo "Waiting... (attempt $i/8)" + done + echo 'Health check failed' + docker logs '${{ vars.RC_CONTAINER }}' --tail 20 + exit 1 + DEPLOY_EOF + + - name: Verify RC instance + run: | + sleep 5 + curl -sf "${{ vars.RC_HEALTH_URL }}" && echo " RC API healthy"