From 4edaf755182185bf3e573a161e7b2990f1fb773b Mon Sep 17 00:00:00 2001 From: Matthijs Laan Date: Mon, 11 Sep 2023 15:19:12 +0200 Subject: [PATCH] Add continuous deployment --- .github/workflows/test-and-deploy.yml | 83 +++++++++++++++++++++++++++ README.md | 35 +++++++++++ ci/docker-compose.pr.yml | 37 ++++++++++++ 3 files changed, 155 insertions(+) create mode 100644 ci/docker-compose.pr.yml diff --git a/.github/workflows/test-and-deploy.yml b/.github/workflows/test-and-deploy.yml index 8b88c2f..52df5b3 100644 --- a/.github/workflows/test-and-deploy.yml +++ b/.github/workflows/test-and-deploy.yml @@ -78,3 +78,86 @@ jobs: with: name: Event File path: ${{ github.event_path }} + + build: + name: 'Build and deploy' + runs-on: ubuntu-22.04 + if: vars.DEPLOY == 'true' + env: + VERSION: snapshot + COMPOSE_PROJECT_NAME: ${{ vars.DEPLOY_PROJECT_NAME }}-snapshot + COMPOSE_FILE: docker-compose.yml:docker-compose.traefik.yml + BASE_HREF: / + HOST: ${{ vars.DEPLOY_HOSTNAME }} + ADMIN_HASHED_PASSWORD: ${{ vars.ADMIN_HASHED_PASSWORD }} + BUILDX_NO_DEFAULT_ATTESTATIONS: 1 + needs: [ lint, test ] + steps: + - uses: actions/checkout@v4 + with: + # If tags aren't fetched the bin/prebuild.js script will leave fields in generated/version.json empty + # https://github.com/actions/checkout/issues/701 + fetch-depth: 0 + - name: 'GitHub Slug Action' + uses: rlespinasse/github-slug-action@v4 + + - name: 'Find Current Pull Request' + uses: jwalton/gh-find-current-pr@v1 + id: find-pr + + - name: 'Set variables for PR' + # When running on a PR, build and tag the Docker image for a deployment with a different base-href and a static-only deployment on + # a path prefix, with the frontend using the /api URL from the main branch deployment. + if: ${{ success() && steps.find-pr.outputs.number }} + env: + PR: ${{ steps.find-pr.outputs.number }} + run: | + echo "VERSION=pr-${PR}" >> $GITHUB_ENV + echo "COMPOSE_PROJECT_NAME=${COMPOSE_PROJECT_NAME}-pr-${PR}" >> $GITHUB_ENV + echo "COMPOSE_FILE=docker-compose.yml:docker-compose.traefik.yml:ci/docker-compose.pr.yml" >> $GITHUB_ENV + echo "BASE_HREF=/pull-request/${PR}/${GITHUB_REF_NAME_SLUG_URL}/" >> $GITHUB_ENV + + - name: 'Build image' + # Always uses the 'snapshot' tag of the tailormap-api base image by setting the API_VERSION build arg + run: docker build --progress plain --build-arg VERSION=${VERSION} --build-arg API_VERSION=snapshot --tag ${{ vars.DEPLOY_IMAGE_TAG }}:${VERSION} --output type=local,dest=image.tar . + + - name: 'Set Docker context for deployment' + uses: arwynfr/actions-docker-context@v2 + with: + docker_host: ${{ secrets.DEPLOY_DOCKER_HOST }} + context_name: 'dev-server' + ssh_cert: ${{ secrets.DEPLOY_DOCKER_HOST_SSH_CERT }} + ssh_key: ${{ secrets.DEPLOY_DOCKER_HOST_SSH_KEY }} + use_context: true + + - name: 'Add known hosts' + run: | + ssh-keyscan -H ${{ secrets.DOCKER_HOSTNAME }} > $HOME/.ssh/known_hosts + + - name: 'Load Docker image' + run: | + docker load --input image.tar + + - uses: actions/checkout@v4 + with: + repository: B3Partners/tailormap-viewer + path: tailormap-viewer + sparse-checkout: | + docker-compose*.yml + sparse-checkout-cone-mode: false + + - name: 'Update deployment using Docker Compose' + run: | + cd tailormap-viewer + docker compose up -d + + - name: 'Create GitHub deployment' + if: success() + uses: chrnorm/deployment-action@v2 + with: + token: "${{ secrets.GITHUB_TOKEN }}" + environment-url: "https://${{ secrets.DEPLOY_HOST }}${{ env.BASE_HREF}}" + description: "Deployment for ${{ env.VERSION }}" + environment: ${{ env.VERSION }} + initial-status: success + ref: "${{ env.GITHUB_HEAD_REF }}" diff --git a/README.md b/README.md index 3d6433f..e208e17 100644 --- a/README.md +++ b/README.md @@ -28,3 +28,38 @@ version matches the NPM packages you use in this Angular app. You can run your customized Tailormap container separately or using the Docker (Compose) configuration in tailormap-viewer. For Docker Compose, specify your custom image and tag in the `TAILORMAP_IMAGE` and `VERSION` variables in an environment file (see the README of tailormap-viewer and its `.env.template` for details). + +# Setting up continuous deployment + +To add continuous deployment, you need a server with Docker and Traefik configured with `--providers.docker` and a Let's Encrypt certificate +resolver named `letsencrypt`. Generate an SSH keypair and add the public key to the `~/.ssh/authorized_keys` file for an account that has +Docker access. Assign a hostname for the deployments to this server. + +You can use different SSH keypairs for different deployments. Just add more public keys to the `authorized_keys` file. + +Add these repository variables in GitHub to enable deployment. + +Like the continuous deployment in `tailormap-viewer`, the Tailormap API backend will only be deployed for the `main` branch and pull request +deployments will only serve the static Angular frontend on a different base path which will use the API for the main deployment on the `/api` +path. + +- `DEPLOY`: set to `true` +- `DEPLOY_HOSTNAME`: set to hostname for the server +- `DEPLOY_PROJECT_NAME`: Name of your customized project, used for docker image and container name (a-z) +- `ADMIN_HASHED_PASSWORD`: Hashed password of the tm-admin account, created when the Tailormap configuration database is empty (only the + first deployment unless you remove the volume manually). Generate with Spring CLI: ` docker run --rm rocko/spring-boot-cli-docker spring encodepassword "[your password]"`. +- `DEPLOY_IMAGE_TAG`: Tag for Docker image (without version), for example `ghcr.io/b3partners/tailormap-viewer`. The image is built in a GitHub Actions worker and uploaded to the server -- it is not pushed to + a registry. The version used is `snapshot` for deployments for the main `branch` and `pr-nn` for pull request deployments. + +Add the following as GitHub secrets: + +- `DEPLOY_DOCKER_HOST`: something like ssh://github-docker-actions@your.server.com +- `DEPLOY_DOCKER_HOST_SSH_CERT`: the public part of the SSH key as added to `authorized_keys`, something like `ssh-rsa AAAAB3NzaC1yc2EAA(...)ei3Uv4zj9/8M= user@host` +- `DEPLOY_DOCKER_HOST_SSH_KEY`: the private part of the SSH key, without passphrase, something like: + +``` +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAA... +... +-----END OPENSSH PRIVATE KEY----- +``` diff --git a/ci/docker-compose.pr.yml b/ci/docker-compose.pr.yml new file mode 100644 index 0000000..fae59ef --- /dev/null +++ b/ci/docker-compose.pr.yml @@ -0,0 +1,37 @@ +# Docker Compose overrides for continuous deployment of a PR with a different BASE_HREF, serving only the static frontend without the +# backend API controllers. Traefik labels configure Tailormap to run on a path prefix. The Angular frontend uses `/api/` (absolute URL) to +# the backend, not a relative URL so the backend of Tailormap for the main branch is used. + +# Usage (see also the GitHub Actions workflows in .github): +# cat << EOF > env-pr +# VERSION=pr-xxx +# BASE_HREF=/pr-xxx/ +# HOST=tailormap.example.com +# COMPOSE_FILE=docker-compose.yml:docker-compose.traefik.yml:ci/docker-compose.pr.yml +# COMPOSE_PROJECT_NAME=tailormap-${VERSION} +# EOF +# docker compose --env-file env-pr build +# docker compose --env-file env-pr up -d +# docker compose --env-file env-pr down -v + +services: + tailormap: + environment: + - SPRING_PROFILES_ACTIVE=static-only + healthcheck: + # With the static-only profile the management port 8081 is disabled so the default HEALTHCHECK won't succeed + test: [NONE] + labels: + - "traefik.http.routers.${COMPOSE_PROJECT_NAME:-tailormap}.rule=Host(`${HOST:-localhost}`) && PathPrefix(`${BASE_HREF}`)" + - "traefik.http.routers.${COMPOSE_PROJECT_NAME:-tailormap}.tls=true" + - "traefik.http.routers.${COMPOSE_PROJECT_NAME:-tailormap}.tls.certresolver=letsencrypt" + - "traefik.http.routers.${COMPOSE_PROJECT_NAME:-tailormap}.middlewares=${COMPOSE_PROJECT_NAME:-tailormap}-stripprefix" + - "traefik.http.services.${COMPOSE_PROJECT_NAME:-tailormap}.loadbalancer.server.port=8080" + - "traefik.http.middlewares.${COMPOSE_PROJECT_NAME:-tailormap}-stripprefix.stripprefix.prefixes=${BASE_HREF}" + restart: unless-stopped + + db: + image: rwgrim/docker-noop + healthcheck: + disable: true + restart: no