diff --git a/.mokogitea/workflows/deploy-dev.yml b/.mokogitea/workflows/deploy-dev.yml index 6e4361c608..f99e1b1069 100644 --- a/.mokogitea/workflows/deploy-dev.yml +++ b/.mokogitea/workflows/deploy-dev.yml @@ -1,25 +1,40 @@ # Copyright (C) 2026 Moko Consulting # SPDX-License-Identifier: GPL-3.0-or-later -# BRIEF: Build and deploy MokoGitea to dev environment on push to dev branch. -# Production deploy (deploy-mokogitea.yml) only succeeds if dev is healthy. +# 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. +# The dev branch is the ongoing integration branch. +# 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): +# 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 MokoGitea (Dev) +name: Deploy (Dev) on: push: branches: - dev + # Manual trigger for isolated end-to-end tests. + # Runs on the ref it is dispatched from. + workflow_dispatch: concurrency: - group: deploy-mokogitea-dev + group: deploy-dev cancel-in-progress: true env: - REGISTRY: git.mokoconsulting.tech - IMAGE: mokoconsulting/mokogitea - DEPLOY_HOST: git.mokoconsulting.tech - DEPLOY_PORT: 2918 - DEPLOY_USER: mokoconsulting FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true jobs: @@ -47,22 +62,21 @@ jobs: echo "$DEPLOY_KEY" > ~/.ssh/deploy_key chmod 600 ~/.ssh/deploy_key - - name: Build and deploy to dev via SSH + - name: Build and deploy to RC via SSH env: REGISTRY_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} TAG: ${{ steps.config.outputs.tag }} run: | - # Inject runner-side values (TAG, REGISTRY_TOKEN) into the remote shell's - # environment via a command prefix, then use a *quoted* heredoc so every - # $var below expands in exactly one place: the remote dev host. This avoids - # the local-vs-remote expansion trap that previously left TAG empty. - ssh -i ~/.ssh/deploy_key -p ${{ env.DEPLOY_PORT }} \ + # 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 \ - ${{ env.DEPLOY_USER }}@${{ env.DEPLOY_HOST }} \ + ${{ 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' + echo 'SSH connected to Dev environment' if [ -z "$TAG" ]; then echo 'ERROR: TAG is empty; refusing to build an untagged image' >&2 @@ -76,52 +90,46 @@ jobs: docker image prune -af 2>/dev/null || true echo 'Pulling source...' - SOURCE_DIR=/opt/gitea-dev/source + SOURCE_DIR='${{ vars.DEV_SOURCE_DIR }}' if [ ! -d "$SOURCE_DIR/.git" ]; then - git clone -b dev https://git.mokoconsulting.tech/MokoConsulting/MokoGitea-Fork.git "$SOURCE_DIR" + git clone -b dev ${{ github.server_url }}/${{ github.repository }}.git "$SOURCE_DIR" fi cd "$SOURCE_DIR" - git remote set-url origin https://git.mokoconsulting.tech/MokoConsulting/MokoGitea-Fork.git 2>/dev/null || true + 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 Docker image: ${{ env.REGISTRY }}/${{ env.IMAGE }}:$TAG" + echo "Building image: ${{ vars.DEV_REGISTRY }}/${{ vars.DEV_IMAGE }}:$TAG" docker build --no-cache --build-arg GOFLAGS='-p 1' \ - --tag "${{ env.REGISTRY }}/${{ env.IMAGE }}:$TAG" \ + --tag "${{ vars.DEV_REGISTRY }}/${{ vars.DEV_IMAGE }}:$TAG" \ -f Dockerfile . echo 'Pushing to registry...' - echo "$REGISTRY_TOKEN" | docker login ${{ env.REGISTRY }} -u ${{ env.DEPLOY_USER }} --password-stdin - docker push "${{ env.REGISTRY }}/${{ env.IMAGE }}:$TAG" + 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 /opt/gitea-dev - # The dev service in the SHARED compose file reads its image tag from - # ${MOKOGITEA_DEV_TAG}. Drive it from the freshly built tag instead of - # rewriting the file with sed: the old sed pattern matched the *prod* - # service line (container_name: mokogitea) and left the dev service pinned - # to a stale image, so every dev deploy recreated old code while silently - # corrupting the prod image pin. The env-var only affects the dev service. - # Remove any lingering fixed-name container first so the recreate can't hit - # a name conflict, pin the project name for determinism, then force-recreate. - docker rm -f mokogitea-dev 2>/dev/null || true - MOKOGITEA_DEV_TAG="$TAG" docker compose -p gitea-dev up -d --force-recreate mokogitea-dev + echo 'Restarting Dev container...' + cd '${{ vars.DEV_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.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" mokogitea-dev 2>/dev/null | grep -q healthy; then + 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 mokogitea-dev --tail 20 + docker logs '${{ vars.DEV_CONTAINER }}' --tail 20 exit 1 DEPLOY_EOF - - name: Verify dev instance + - name: Verify Dev instance run: | sleep 5 - curl -sf https://git.dev.mokoconsulting.tech/api/healthz && echo " Dev API healthy" + curl -sf "${{ vars.DEV_HEALTH_URL }}" && echo " Dev API healthy" diff --git a/.mokogitea/workflows/deploy-mokogitea.yml b/.mokogitea/workflows/deploy-mokogitea.yml deleted file mode 100644 index 0af8f78ca3..0000000000 --- a/.mokogitea/workflows/deploy-mokogitea.yml +++ /dev/null @@ -1,162 +0,0 @@ -# Copyright (C) 2026 Moko Consulting -# SPDX-License-Identifier: GPL-3.0-or-later -# BRIEF: Build MokoGitea Docker image, push to registry, and deploy - -name: Deploy MokoGitea - -on: - push: - branches: - - main - workflow_dispatch: - inputs: - version: - description: 'Version tag' - required: true - default: 'latest' - environment: - description: 'Target environment' - required: true - default: 'dev' - type: choice - options: - - dev - - production - -concurrency: - group: deploy-mokogitea - cancel-in-progress: false - -env: - REGISTRY: git.mokoconsulting.tech - IMAGE: mokoconsulting/mokogitea - DEPLOY_HOST: git.mokoconsulting.tech - DEPLOY_PORT: 2918 - DEPLOY_USER: mokoconsulting - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - -jobs: - deploy: - runs-on: ubuntu-latest - steps: - # NOTE: removed a "Verify dev environment is healthy" gate that curled - # git.dev and `exit 1` on failure — gating a PROD deploy on a live DEV - # health check is a cross-tier false-negative (a transient dev blip blocked - # prod). Pre-prod validation is the dev -> rc -> main pipeline + RC env. - - name: Checkout source (for version detection) - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Determine settings - id: config - run: | - if [ "${{ github.event_name }}" = "push" ]; then - VERSION=$(git describe --tags --always 2>/dev/null || echo "dev-$(git rev-parse --short HEAD)") - ENV="production" - else - VERSION="${{ github.event.inputs.version }}" - ENV="${{ github.event.inputs.environment }}" - fi - if [ "$ENV" = "production" ]; then - echo "compose_dir=/opt/gitea-dev" >> $GITHUB_OUTPUT - echo "container=mokogitea" >> $GITHUB_OUTPUT - echo "source_dir=/opt/gitea/source" >> $GITHUB_OUTPUT - echo "branch=main" >> $GITHUB_OUTPUT - echo "tag=$VERSION" >> $GITHUB_OUTPUT - echo "tag_env=MOKOGITEA_TAG" >> $GITHUB_OUTPUT - else - echo "compose_dir=/opt/gitea-dev" >> $GITHUB_OUTPUT - echo "container=mokogitea-dev" >> $GITHUB_OUTPUT - echo "source_dir=/opt/gitea-dev/source" >> $GITHUB_OUTPUT - echo "branch=dev" >> $GITHUB_OUTPUT - echo "tag=$VERSION-dev" >> $GITHUB_OUTPUT - echo "tag_env=MOKOGITEA_DEV_TAG" >> $GITHUB_OUTPUT - fi - - - 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 via SSH - env: - REGISTRY_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} - TAG: ${{ steps.config.outputs.tag }} - BRANCH: ${{ steps.config.outputs.branch }} - SOURCE_DIR: ${{ steps.config.outputs.source_dir }} - COMPOSE_DIR: ${{ steps.config.outputs.compose_dir }} - CONTAINER: ${{ steps.config.outputs.container }} - TAG_ENV: ${{ steps.config.outputs.tag_env }} - run: | - HEALTH_FMT='${{ '{{' }}.State.Health.Status${{ '}}' }}' - IMAGE_FMT='Image: ${{ '{{' }}.Config.Image${{ '}}' }}' - - ssh -i ~/.ssh/deploy_key -p ${{ env.DEPLOY_PORT }} \ - -o ConnectTimeout=30 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \ - -o ServerAliveInterval=30 -o ServerAliveCountMax=10 \ - ${{ env.DEPLOY_USER }}@${{ env.DEPLOY_HOST }} bash -s </dev/null || true - docker image prune -af 2>/dev/null || true - free -m | head -3 - - echo 'Pulling source...' - if [ ! -d $SOURCE_DIR/.git ]; then - git clone -b $BRANCH https://git.mokoconsulting.tech/MokoConsulting/MokoGitea-Fork.git $SOURCE_DIR - fi - cd $SOURCE_DIR - # Ensure remote points to MokoGitea-Fork (not the upstream fork) - git remote set-url origin https://git.mokoconsulting.tech/MokoConsulting/MokoGitea-Fork.git 2>/dev/null || true - git fetch origin $BRANCH - git reset --hard origin/$BRANCH - - echo 'Building Docker image...' - docker build --no-cache --build-arg GOFLAGS='-p 1' \ - --tag ${{ env.REGISTRY }}/${{ env.IMAGE }}:$TAG \ - --tag ${{ env.REGISTRY }}/${{ env.IMAGE }}:latest \ - -f Dockerfile . - - echo 'Pushing to registry...' - echo '$REGISTRY_TOKEN' | docker login ${{ env.REGISTRY }} -u ${{ env.DEPLOY_USER }} --password-stdin - docker push ${{ env.REGISTRY }}/${{ env.IMAGE }}:$TAG - docker push ${{ env.REGISTRY }}/${{ env.IMAGE }}:latest - - echo 'Restarting container...' - cd $COMPOSE_DIR - # Drive ONLY the target service's tag via its compose env-var (no sed — - # a blanket 'sed s|mokogitea:...|' clobbered the dev + rc service tags too). - # Remove any lingering fixed-name container first (avoids the "name already - # in use" conflict), then force-recreate under the shared compose project. - docker rm -f $CONTAINER 2>/dev/null || true - env $TAG_ENV="$TAG" docker compose -p gitea-dev up -d --force-recreate $CONTAINER - - echo 'Health check...' - for i in 1 2 3 4 5 6 7 8; do - sleep 15 - if docker inspect --format='$HEALTH_FMT' $CONTAINER 2>/dev/null | grep -q healthy; then - echo 'Container healthy!' - docker inspect --format='$IMAGE_FMT' $CONTAINER - exit 0 - fi - echo "Waiting... (attempt \$i/8)" - done - echo 'Health check failed' - docker logs $CONTAINER --tail 20 - exit 1 - DEPLOY_EOF - - - name: Verify - run: | - sleep 5 - curl -sf https://${{ env.DEPLOY_HOST }}/api/healthz && echo " API healthy" - - - name: Notify on failure - if: failure() - run: echo "::error::Deploy failed for ${{ steps.config.outputs.tag }}" diff --git a/.mokogitea/workflows/deploy-prod.yml b/.mokogitea/workflows/deploy-prod.yml new file mode 100644 index 0000000000..60aea36faa --- /dev/null +++ b/.mokogitea/workflows/deploy-prod.yml @@ -0,0 +1,135 @@ +# Copyright (C) 2026 Moko Consulting +# SPDX-License-Identifier: GPL-3.0-or-later +# BRIEF: Build and deploy to the Prod 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. +# Prod deploys on merge to main (dev -> rc -> main pipeline). +# 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): +# 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 a prod re-deploy. + # Runs on the ref it is dispatched from (use main). + 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 "$(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 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.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" + docker build --no-cache --build-arg GOFLAGS='-p 1' \ + --tag "${{ vars.PROD_REGISTRY }}/${{ vars.PROD_IMAGE }}:$TAG" \ + -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" + + echo 'Restarting Prod container...' + cd '${{ vars.PROD_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.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"