diff --git a/.mokogitea/workflows/deploy-dev.yml b/.mokogitea/workflows/deploy-dev.yml new file mode 100644 index 0000000..a58bd9a --- /dev/null +++ b/.mokogitea/workflows/deploy-dev.yml @@ -0,0 +1,145 @@ +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: MokoGitea.Workflow +# INGROUP: MokoStandards.Deploy +# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Go +# PATH: /.mokogitea/workflows/deploy-dev.yml +# VERSION: 01.00.00 +# BRIEF: Build and deploy to the dev environment on push to the dev branch. +# Portable across Go server repos: ALL deployment config comes from repo +# Actions variables (vars.*) and secrets (secrets.*), nothing hardcoded. +# Canonical source — syncs to root .mokogitea/workflows/ of Go server +# repos (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): +# DEV_SSH_HOST, DEV_SSH_PORT, DEV_SSH_USERNAME - SSH deploy target for the dev tier +# DEV_REGISTRY, DEV_REGISTRY_USER, DEV_IMAGE - container registry + login user + image +# DEV_CONTAINER - compose service/container to recreate +# DEV_COMPOSE_PROJECT - docker compose -p project name +# DEV_COMPOSE_DIR - dir containing docker-compose.yml on host +# DEV_SOURCE_DIR - build source checkout on host +# DEV_TAG_ENV - compose env-var name that pins the image tag +# DEV_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 (Dev) + +on: + push: + branches: + - dev + # Manual trigger for isolated end-to-end tests without a full dev push. + # Runs on the ref it is dispatched from; that ref must carry current source + # (>= the dev database migration version) or the rebuilt image will refuse the + # newer DB. Dispatch from `dev` once `dev` is current. + workflow_dispatch: + +concurrency: + group: deploy-dev + cancel-in-progress: true + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +jobs: + deploy-dev: + name: "Build & Deploy to Dev" + 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 "dev-$(git rev-parse --short HEAD)") + echo "tag=${VERSION}-dev" >> $GITHUB_OUTPUT + echo "Version: ${VERSION}-dev" + + - 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 dev 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.DEV_SSH_PORT }} \ + -o ConnectTimeout=30 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \ + -o ServerAliveInterval=30 -o ServerAliveCountMax=10 \ + ${{ vars.DEV_SSH_USERNAME }}@${{ vars.DEV_SSH_HOST }} \ + "TAG='$TAG' REGISTRY_TOKEN='$REGISTRY_TOKEN' bash -s" <<'DEPLOY_EOF' + set -e + echo 'SSH connected to dev 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.DEV_SOURCE_DIR }}' + if [ ! -d "$SOURCE_DIR/.git" ]; then + git clone -b dev ${{ 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 dev + git reset --hard origin/dev + + echo "Building image: ${{ vars.DEV_REGISTRY }}/${{ vars.DEV_IMAGE }}:$TAG" + docker build --no-cache --build-arg GOFLAGS='-p 1' \ + --tag "${{ vars.DEV_REGISTRY }}/${{ vars.DEV_IMAGE }}:$TAG" \ + -f Dockerfile . + + echo 'Pushing to registry...' + echo "$REGISTRY_TOKEN" | docker login ${{ vars.DEV_REGISTRY }} -u ${{ vars.DEV_REGISTRY_USER }} --password-stdin + docker push "${{ vars.DEV_REGISTRY }}/${{ vars.DEV_IMAGE }}:$TAG" + + echo 'Restarting dev container...' + cd '${{ vars.DEV_COMPOSE_DIR }}' + # Drive the dev 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.DEV_CONTAINER }}' 2>/dev/null || true + ${{ vars.DEV_TAG_ENV }}="$TAG" docker compose -p '${{ vars.DEV_COMPOSE_PROJECT }}' up -d --force-recreate '${{ vars.DEV_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.DEV_CONTAINER }}' 2>/dev/null | grep -q healthy; then + echo 'Dev container healthy!' + exit 0 + fi + echo "Waiting... (attempt $i/8)" + done + echo 'Health check failed' + docker logs '${{ vars.DEV_CONTAINER }}' --tail 20 + exit 1 + DEPLOY_EOF + + - name: Verify dev instance + run: | + sleep 5 + curl -sf "${{ vars.DEV_HEALTH_URL }}" && echo " Dev API healthy" diff --git a/.mokogitea/workflows/deploy-prod.yml b/.mokogitea/workflows/deploy-prod.yml new file mode 100644 index 0000000..a7d0267 --- /dev/null +++ b/.mokogitea/workflows/deploy-prod.yml @@ -0,0 +1,153 @@ +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: MokoGitea.Workflow +# INGROUP: MokoStandards.Deploy +# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Go +# PATH: /.mokogitea/workflows/deploy-prod.yml +# VERSION: 01.00.00 +# BRIEF: Build and deploy to the production environment on push to the main branch. +# Portable across Go server repos: ALL deployment config comes from repo +# Actions variables (vars.*) and secrets (secrets.*), nothing hardcoded. +# Also publishes the :latest tag (prod-only) alongside the versioned tag. +# Canonical source — syncs to root .mokogitea/workflows/ of Go server +# repos (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): +# PROD_SSH_HOST, PROD_SSH_PORT, PROD_SSH_USERNAME - SSH deploy target for the prod tier +# PROD_REGISTRY, PROD_REGISTRY_USER, PROD_IMAGE - container registry + login user + image +# PROD_CONTAINER - compose service/container to recreate +# PROD_COMPOSE_PROJECT - docker compose -p project name +# PROD_COMPOSE_DIR - dir containing docker-compose.yml on host +# PROD_SOURCE_DIR - build source checkout on host +# PROD_TAG_ENV - compose env-var name that pins the image tag +# PROD_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 (Prod) + +on: + push: + branches: + - main + # Manual trigger for isolated redeploys of the current main HEAD. + # Runs on the ref it is dispatched from; that ref must carry current source + # (>= the prod database migration version) or the rebuilt image will refuse the + # newer DB. Dispatch from `main` once `main` is current. + workflow_dispatch: + +concurrency: + group: deploy-prod + cancel-in-progress: false + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +jobs: + deploy-prod: + name: "Build & Deploy to Prod" + 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 "prod-$(git rev-parse --short HEAD)") + echo "tag=${VERSION}" >> $GITHUB_OUTPUT + echo "Version: ${VERSION}" + + - 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 prod 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.PROD_SSH_PORT }} \ + -o ConnectTimeout=30 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \ + -o ServerAliveInterval=30 -o ServerAliveCountMax=10 \ + ${{ vars.PROD_SSH_USERNAME }}@${{ vars.PROD_SSH_HOST }} \ + "TAG='$TAG' REGISTRY_TOKEN='$REGISTRY_TOKEN' bash -s" <<'DEPLOY_EOF' + set -e + echo 'SSH connected to prod 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.PROD_SOURCE_DIR }}' + if [ ! -d "$SOURCE_DIR/.git" ]; then + git clone -b main ${{ 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 main + git reset --hard origin/main + + echo "Building image: ${{ vars.PROD_REGISTRY }}/${{ vars.PROD_IMAGE }}:$TAG (+ :latest)" + # Prod publishes both the versioned tag and a moving :latest tag. + docker build --no-cache --build-arg GOFLAGS='-p 1' \ + --tag "${{ vars.PROD_REGISTRY }}/${{ vars.PROD_IMAGE }}:$TAG" \ + --tag "${{ vars.PROD_REGISTRY }}/${{ vars.PROD_IMAGE }}:latest" \ + -f Dockerfile . + + echo 'Pushing to registry...' + echo "$REGISTRY_TOKEN" | docker login ${{ vars.PROD_REGISTRY }} -u ${{ vars.PROD_REGISTRY_USER }} --password-stdin + docker push "${{ vars.PROD_REGISTRY }}/${{ vars.PROD_IMAGE }}:$TAG" + docker push "${{ vars.PROD_REGISTRY }}/${{ vars.PROD_IMAGE }}:latest" + + echo 'Restarting prod container...' + cd '${{ vars.PROD_COMPOSE_DIR }}' + # Drive the prod 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.PROD_CONTAINER }}' 2>/dev/null || true + ${{ vars.PROD_TAG_ENV }}="$TAG" docker compose -p '${{ vars.PROD_COMPOSE_PROJECT }}' up -d --force-recreate '${{ vars.PROD_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.PROD_CONTAINER }}' 2>/dev/null | grep -q healthy; then + echo 'Prod container healthy!' + exit 0 + fi + echo "Waiting... (attempt $i/8)" + done + echo 'Health check failed' + docker logs '${{ vars.PROD_CONTAINER }}' --tail 20 + exit 1 + DEPLOY_EOF + + - name: Verify prod instance + run: | + sleep 5 + curl -sf "${{ vars.PROD_HEALTH_URL }}" && echo " Prod API healthy" + + - name: Notify on failure + if: failure() + run: echo "::error::Prod deploy failed for ${{ steps.config.outputs.tag }}" diff --git a/.mokogitea/workflows/deploy-rc.yml b/.mokogitea/workflows/deploy-rc.yml new file mode 100644 index 0000000..9b533cb --- /dev/null +++ b/.mokogitea/workflows/deploy-rc.yml @@ -0,0 +1,146 @@ +# Copyright (C) 2026 Moko Consulting +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# FILE INFORMATION +# DEFGROUP: MokoGitea.Workflow +# INGROUP: MokoStandards.Deploy +# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Go +# PATH: /.mokogitea/workflows/deploy-rc.yml +# VERSION: 01.00.00 +# 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. +# Canonical source — syncs to root .mokogitea/workflows/ of Go server +# repos (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 + # Manual trigger for isolated end-to-end tests without a full RC promotion. + # Runs on the ref it is dispatched from; that ref must carry current source + # (>= the RC database migration version) or the rebuilt image will refuse the + # newer DB. Dispatch from `rc` once `rc` is current. + workflow_dispatch: + +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"